--- title: "Animation" description: "Add motion to designs with entrance, exit, and loop animation presets, timing controls, and programmatic APIs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/animation/overview-6a2ef2/) - Add motion to video scenes with preset animation controls and programmatic animation APIs. - [Supported Animation Types](https://img.ly/docs/cesdk/mac-catalyst/animation/types-4e5f41/) - Apply different animation types to design blocks in CE.SDK and configure their properties. - [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/) - Build entrance, exit, loop, and text animations with CE.SDK on Apple platforms. - [Edit Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/edit-32c12a/) - Modify existing animations in CE.SDK by reading properties, changing duration and easing, adjusting direction, and replacing or removing animations from blocks. - [Composition Groups](https://img.ly/docs/cesdk/mac-catalyst/animation/composition-groups-20bae2/) - Give the children of a group their own clock, run them faster with the group speed, and start them with the group. - [Time Remap](https://img.ly/docs/cesdk/mac-catalyst/animation/time-remap-063ab0/) - Freeze, reverse or replay the children of a composition group by remapping their clock with a static value or keyframes. - [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/) - Animate single block properties over time with keyframes, easing curves and absolute or relative tracks. - [Follow Another Block](https://img.ly/docs/cesdk/mac-catalyst/animation/transform-parent-b51eab/) - Make a block follow the position, rotation and scale of another block with a transform parent, also while that block is animated. - [Text Animators](https://img.ly/docs/cesdk/mac-catalyst/animation/text-animators-97387e/) - Animate the characters, words or lines of a text block with text animators, range selectors and keyframes. - [Motion Paths](https://img.ly/docs/cesdk/mac-catalyst/animation/motion-paths-7c619f/) - Move a block along a straight or curved path with vec2 position keyframes and spatial tangents. - [Motion Blur](https://img.ly/docs/cesdk/mac-catalyst/animation/motion-blur-122f43/) - Blur moving blocks in exported videos and in the preview with a shutter angle, a shutter phase and a number of samples. - [Anchor, Skew and Auto-Orient](https://img.ly/docs/cesdk/mac-catalyst/animation/anchor-skew-auto-orient-ede5e9/) - Set the point that a block turns, scales and skews about, shear a block with a skew, and turn a block along its motion path. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Anchor, Skew and Auto-Orient" description: "Set the point that a block turns, scales and skews about, shear a block with a skew, and turn a block along its motion path." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/anchor-skew-auto-orient-ede5e9/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Anchor, Skew and Auto-Orient](https://img.ly/docs/cesdk/mac-catalyst/animation/anchor-skew-auto-orient-ede5e9/) --- ```swift file=@cesdk_swift_examples/engine-guides-anchor-skew-auto-orient/AnchorSkewAutoOrient.swift reference-only import Foundation import IMGLYEngine struct AnchorSkewAutoOrientGuideResult { let centerAnchorX: Float let signSkew: Float let arrowAutoOrient: Bool let anchorErrorCode: EngineErrorCode? let autoOrientErrorCode: EngineErrorCode? let pngData: Data } @MainActor func anchorSkewAutoOrient(engine: Engine) async throws -> AnchorSkewAutoOrientGuideResult { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) try engine.block.setDuration(page, duration: 5) // Creates a rect graphic with a color fill on the page func createRect(x: Float, y: Float, width: Float, height: Float, color: Color) throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: color) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: width) try engine.block.setHeight(block, value: height) try engine.block.setPositionX(block, value: x) try engine.block.setPositionY(block, value: y) try engine.block.appendChild(to: page, child: block) try engine.block.setDuration(block, duration: 5) return block } let cornerSquare = try createRect(x: 300, y: 200, width: 160, height: 160, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) let centerSquare = try createRect(x: 700, y: 200, width: 160, height: 160, color: .rgba(r: 0.9, g: 0.3, b: 0.4, a: 1)) // Without an anchor, a rotation track turns a block about its center, so // the left square sets its anchor to its top-left corner try engine.block.setFloat(cornerSquare, property: "transform/anchor/x", value: 0) try engine.block.setFloat(cornerSquare, property: "transform/anchor/y", value: 0) try engine.block.setFloat(centerSquare, property: "transform/anchor/x", value: 80) try engine.block.setFloat(centerSquare, property: "transform/anchor/y", value: 80) for square in [cornerSquare, centerSquare] { try engine.block.setKeyframes(square, property: "rotation", keyframes: [ Keyframe(time: 0, value: .float(0)), Keyframe(time: 1, value: .float(Float.pi / 2)), ]) } let centerAnchorX = try engine.block.getFloat(centerSquare, property: "transform/anchor/x") print(centerAnchorX) // 80.0 // An anchor set to 0 is still an anchor print(try engine.block.hasTransformAnchor(cornerSquare)) // true let sign = try createRect(x: 1150, y: 200, width: 240, height: 160, color: .rgba(r: 0.95, g: 0.7, b: 0.1, a: 1)) try engine.block.setFloat(sign, property: "transform/skew", value: .pi / 9) // 20 degrees try engine.block.setFloat(sign, property: "transform/skewAxis", value: 0) // The anchor sits at the bottom center, so the base of the blade stays put let blade = try createRect(x: 1600, y: 600, width: 40, height: 300, color: .rgba(r: 0.2, g: 0.7, b: 0.3, a: 1)) try engine.block.setFloat(blade, property: "transform/anchor/x", value: 20) try engine.block.setFloat(blade, property: "transform/anchor/y", value: 300) try engine.block.setKeyframes(blade, property: "transform/skew", keyframes: [ Keyframe(time: 0, value: .float(-.pi / 9)), Keyframe(time: 0.5, value: .float(.pi / 9)), Keyframe(time: 1, value: .float(-.pi / 9)), ]) let arrow = try createRect(x: 300, y: 800, width: 160, height: 40, color: .rgba(r: 0.5, g: 0.2, b: 0.8, a: 1)) try engine.block.setFloat(arrow, property: "transform/anchor/x", value: 80) try engine.block.setFloat(arrow, property: "transform/anchor/y", value: 20) try engine.block.setVec2(arrow, property: "position", value: [300, 800]) try engine.block.setKeyframes(arrow, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([300, 800]), spatialOut: [300, -300], spatialIn: [-300, -300]), Keyframe(time: 1, value: .vec2([1300, 800])), ]) try engine.block.setBool(arrow, property: "transform/autoOrient", value: true) let arrowAutoOrient = try engine.block.getBool(arrow, property: "transform/autoOrient") print(arrowAutoOrient) // true var anchorErrorCode: EngineErrorCode? do { // A page has no anchor and no skew try engine.block.setFloat(page, property: "transform/anchor/x", value: 10) } catch let error as EngineError { anchorErrorCode = error.catalogCode print(anchorErrorCode == .blockTypeCannotHaveAnchorOrSkew) // true } var autoOrientErrorCode: EngineErrorCode? do { try engine.block.setBool(page, property: "transform/autoOrient", value: true) } catch let error as EngineError { autoOrientErrorCode = error.catalogCode print(autoOrientErrorCode == .blockTypeCannotAutoOrient) // true } // Export the frame at 1.5 seconds, while the arrow climbs its path try engine.block.setPlaybackTime(page, time: 1.5) let pngData = try await engine.block.export(page, mimeType: .png) try await engine.captureGuide(data: pngData, label: "hero", mimeType: .png) return try AnchorSkewAutoOrientGuideResult( centerAnchorX: centerAnchorX, signSkew: engine.block.getFloat(sign, property: "transform/skew"), arrowAutoOrient: arrowAutoOrient, anchorErrorCode: anchorErrorCode, autoOrientErrorCode: autoOrientErrorCode, pngData: pngData, ) } ``` Set the point that a block turns, scales and skews about, shear a block with a skew, and turn a block along its motion path. ![Two turned squares, a leaning sign, a swaying blade of grass and a purple bar tilted along its rising path, at 1.5 seconds](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-anchor-skew-auto-orient) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Anchor, skew and auto-orient are transform properties of graphic, text and caption blocks. The anchor is the pivot of rotation, scale and skew, the skew shears a block, and auto-orient turns a block along the direction of its position keyframes. The editor has no controls for them, so you set them with the block API. This guide covers how to set the anchor, how to skew a block and animate the skew, how to turn a block along its motion path, the errors to expect, and how the properties behave with other block operations. It ends with a frame export. ## Creating the Blocks The example draws its blocks as rectangles with a color fill. A small local function creates each one on the page with a duration of 5 seconds. ```swift highlight-anchorSkewAutoOrient-createRect // Creates a rect graphic with a color fill on the page func createRect(x: Float, y: Float, width: Float, height: Float, color: Color) throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: color) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: width) try engine.block.setHeight(block, value: height) try engine.block.setPositionX(block, value: x) try engine.block.setPositionY(block, value: y) try engine.block.appendChild(to: page, child: block) try engine.block.setDuration(block, duration: 5) return block } ``` ## Setting the Anchor `transform/anchor/x` and `transform/anchor/y` set the point that rotation, scale and skew pivot on, in design units from the top-left corner of the block's frame. A block without an anchor turns about its center under a `rotation` track, as `setRotation(_:radians:)` turns it. Its skew and scale pivot on its top-left corner. Both squares get the same quarter turn over 5 seconds. The left square sets its anchor to (0, 0) and swings about its corner, while the right square has its anchor at its center and turns in place. `setFloat(_:property:value:)` sets the anchor and `getFloat(_:property:)` reads it back. ```swift highlight-anchorSkewAutoOrient-anchor let cornerSquare = try createRect(x: 300, y: 200, width: 160, height: 160, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) let centerSquare = try createRect(x: 700, y: 200, width: 160, height: 160, color: .rgba(r: 0.9, g: 0.3, b: 0.4, a: 1)) // Without an anchor, a rotation track turns a block about its center, so // the left square sets its anchor to its top-left corner try engine.block.setFloat(cornerSquare, property: "transform/anchor/x", value: 0) try engine.block.setFloat(cornerSquare, property: "transform/anchor/y", value: 0) try engine.block.setFloat(centerSquare, property: "transform/anchor/x", value: 80) try engine.block.setFloat(centerSquare, property: "transform/anchor/y", value: 80) for square in [cornerSquare, centerSquare] { try engine.block.setKeyframes(square, property: "rotation", keyframes: [ Keyframe(time: 0, value: .float(0)), Keyframe(time: 1, value: .float(Float.pi / 2)), ]) } let centerAnchorX = try engine.block.getFloat(centerSquare, property: "transform/anchor/x") print(centerAnchorX) // 80.0 ``` Setting `transform/anchor/x` or `transform/anchor/y`, even to 0, or giving one of them keyframes, gives a block an anchor. `getFloat(_:property:)` reads 0 for a block without an anchor too, so `hasTransformAnchor(_:)` tells whether a block has one. ```swift highlight-anchorSkewAutoOrient-hasAnchor // An anchor set to 0 is still an anchor print(try engine.block.hasTransformAnchor(cornerSquare)) // true ``` ## Skewing a Block `transform/skew` shears a block by an angle in radians, like `rotation`. `transform/skewAxis` sets the direction of the shear in radians, where 0 shears along the block's x-axis. The rendered shear is limited to the range from -85° to 85°, about -1.48 to 1.48 radians. The skew pivots on the anchor, or on the top-left corner of a block without one. The sign has no anchor, so it keeps its top edge in place and leans by 20 degrees, `.pi / 9` radians. ```swift highlight-anchorSkewAutoOrient-skew let sign = try createRect(x: 1150, y: 200, width: 240, height: 160, color: .rgba(r: 0.95, g: 0.7, b: 0.1, a: 1)) try engine.block.setFloat(sign, property: "transform/skew", value: .pi / 9) // 20 degrees try engine.block.setFloat(sign, property: "transform/skewAxis", value: 0) ``` Angles are in radians by default (`.rad`). To work in degrees, switch the scene's angle unit with `engine.scene.setAngleUnit(.deg)`. Then every angle property, such as the skew, the skew axis and `rotation`, its keyframe values, and `setRotation(_:radians:)` and `getRotation(_:)` take and return degrees. `engine.scene.getAngleUnit()` reads the unit back. Scene files keep their format, so the setting only changes the values you pass and read. CE.SDK uses radians for every angle property, in memory and in the API. Use `.deg` only to keep an older integration working. `motionBlur/shutterAngle`, `motionBlur/shutterPhase` and the `rotate` action take degrees in either unit. The CE.SDK editor UI expects radians. Changing the unit also converts a number variable bound only to angle properties. The HTML5, IDML, PSD, EPS, PPTX, PDF and Lottie importers and exporters work in either unit. ```swift try engine.scene.setAngleUnit(.deg) ``` ## Animating the Skew All four anchor and skew properties are numbers, so `.float` keyframes animate them with `setKeyframes(_:property:keyframes:mode:)`. The blade of grass has its anchor at its bottom center, so its base stays in place while the skew track sways it from -20 to 20 degrees (`-.pi / 9` to `.pi / 9`) and back. For keyframes, see [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/). ```swift highlight-anchorSkewAutoOrient-animateSkew // The anchor sits at the bottom center, so the base of the blade stays put let blade = try createRect(x: 1600, y: 600, width: 40, height: 300, color: .rgba(r: 0.2, g: 0.7, b: 0.3, a: 1)) try engine.block.setFloat(blade, property: "transform/anchor/x", value: 20) try engine.block.setFloat(blade, property: "transform/anchor/y", value: 300) try engine.block.setKeyframes(blade, property: "transform/skew", keyframes: [ Keyframe(time: 0, value: .float(-.pi / 9)), Keyframe(time: 0.5, value: .float(.pi / 9)), Keyframe(time: 1, value: .float(-.pi / 9)), ]) ``` ## Turning Along the Motion Path When the bool property `transform/autoOrient` is on, a block turns to follow the direction of its position keyframes. The engine adds the direction of the path at the current playback time to the block's rotation, so a static rotation or a rotation track stays as an offset. The direction comes from a `position` track or from `position/x` and `position/y` tracks, absolute or relative. A hold segment adds no turn. The arrow turns about its anchor at its center, so it points up while it climbs the curve and down while it falls. ```swift highlight-anchorSkewAutoOrient-autoOrient let arrow = try createRect(x: 300, y: 800, width: 160, height: 40, color: .rgba(r: 0.5, g: 0.2, b: 0.8, a: 1)) try engine.block.setFloat(arrow, property: "transform/anchor/x", value: 80) try engine.block.setFloat(arrow, property: "transform/anchor/y", value: 20) try engine.block.setVec2(arrow, property: "position", value: [300, 800]) try engine.block.setKeyframes(arrow, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([300, 800]), spatialOut: [300, -300], spatialIn: [-300, -300]), Keyframe(time: 1, value: .vec2([1300, 800])), ]) try engine.block.setBool(arrow, property: "transform/autoOrient", value: true) let arrowAutoOrient = try engine.block.getBool(arrow, property: "transform/autoOrient") print(arrowAutoOrient) // true ``` `transform/autoOrient` is off by default and does not take keyframes. `setBool(_:property:value:)` turns it on and `getBool(_:property:)` reads it back. ## Handling Errors Graphic, text and caption blocks have an anchor, a skew and auto-orient. Any other block, such as a page or a group, is refused and nothing changes. The call throws an `EngineError` whose `catalogCode` names the cause. ```swift highlight-anchorSkewAutoOrient-errors var anchorErrorCode: EngineErrorCode? do { // A page has no anchor and no skew try engine.block.setFloat(page, property: "transform/anchor/x", value: 10) } catch let error as EngineError { anchorErrorCode = error.catalogCode print(anchorErrorCode == .blockTypeCannotHaveAnchorOrSkew) // true } var autoOrientErrorCode: EngineErrorCode? do { try engine.block.setBool(page, property: "transform/autoOrient", value: true) } catch let error as EngineError { autoOrientErrorCode = error.catalogCode print(autoOrientErrorCode == .blockTypeCannotAutoOrient) // true } ``` ## Exporting a Frame We export the page as PNG at 1.5 seconds. The frame shows both squares part way through their turn, the leaning sign, the swaying blade and the arrow pointing up along its path. ```swift highlight-anchorSkewAutoOrient-export // Export the frame at 1.5 seconds, while the arrow climbs its path try engine.block.setPlaybackTime(page, time: 1.5) let pngData = try await engine.block.export(page, mimeType: .png) ``` ## Anchor, Skew and Other Block Operations - `setRotation(_:radians:)` keeps the center of an anchored block in place. - Scaling a block scales its anchor and keeps the fixed corner. A flip mirrors an anchored or skewed block in place. Filling the parent removes the skew. - Saving and loading and undo keep the properties. Changing the design unit converts the anchor. - A block that follows a transform parent auto-orients in the space of that parent. - When the engine enforces scopes, changing the anchor needs the `layer/move` scope, and changing the skew or auto-orient needs `layer/rotate`. Check a block with `isAllowedByScope` before you offer the edit. ## Troubleshooting - **The block type has no anchor or skew** (`EngineErrorCode.blockTypeCannotHaveAnchorOrSkew`): pass a graphic, text or caption block. - **The block type cannot auto-orient** (`EngineErrorCode.blockTypeCannotAutoOrient`): pass a graphic, text or caption block. - **The anchor or the skew cannot change** (`EngineErrorCode.blockTransformLockedAnchorOrSkew`): the transform of the block is locked. Unlock it first. - **Auto-orient cannot change** (`EngineErrorCode.blockTransformLockedRotate`): the transform of the block is locked. Unlock it first. - **The value is not a number** (`EngineErrorCode.editorValueNotFinite`): pass a finite anchor, skew or skew axis. - **The block turns about its corner**: it has an anchor at (0, 0), which `hasTransformAnchor(_:)` reports. A block with a `rotation` track in a scene saved by an earlier version loads with this anchor. Set the anchor to half the width and height to turn the block about its center. - **The block does not turn along its path**: it has no position keyframes, or the current segment is a hold. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.create(_:)` | Create a graphic block | | `engine.block.createShape(_:)` | Create the rect shape | | `engine.block.setShape(_:shape:)` | Give a graphic its shape | | `engine.block.createFill(_:)` | Create a color fill | | `engine.block.setColor(_:property:color:)` | Set the fill color | | `engine.block.setFill(_:fill:)` | Give a graphic its fill | | `engine.block.setWidth(_:value:)` | Set the width of a block | | `engine.block.setHeight(_:value:)` | Set the height of a block | | `engine.block.setPositionX(_:value:)` | Set the x position of a block | | `engine.block.setPositionY(_:value:)` | Set the y position of a block | | `engine.block.appendChild(to:child:)` | Add a block to the page | | `engine.block.setDuration(_:duration:)` | Set the duration that keyframe times refer to | | `engine.block.setFloat(_:property:value:)` | Set the anchor, the skew or the skew axis | | `engine.block.getFloat(_:property:)` | Read an anchor or skew property | | `engine.block.hasTransformAnchor(_:)` | Check whether a block has an anchor, even one at (0, 0) | | `engine.block.setBool(_:property:value:)` | Turn auto-orient on or off | | `engine.block.getBool(_:property:)` | Check whether a block auto-orients | | `engine.block.setKeyframes(_:property:keyframes:mode:)` | Animate the rotation, the skew or the position | | `engine.scene.setAngleUnit(_:)` | Take and return angles in `.rad` (default) or `.deg` | | `engine.block.setVec2(_:property:value:)` | Set the static position at the start of the path | | `engine.block.setPlaybackTime(_:time:)` | Seek before you export a frame | | `engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:)` | Export the frame as PNG | ### Properties | Property | Type | Description | | --- | --- | --- | | `transform/anchor/x` | Float | X position of the anchor, in design units from the left edge of the frame. Reads 0 for a block without an anchor | | `transform/anchor/y` | Float | Y position of the anchor, in design units from the top edge of the frame. Reads 0 for a block without an anchor | | `transform/skew` | Float | Shear angle in radians, rendered from -85° to 85° (about ±1.48 radians). Default 0 | | `transform/skewAxis` | Float | Direction of the shear in radians, 0 shears along the x-axis. Default 0 | | `transform/autoOrient` | Bool | Turn the block along the direction of its position keyframes. Default off | ## Next Steps - [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/): animate block properties with keyframe tracks. - [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/): add preset animations, which turn and scale about the anchor. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Composition Groups" description: "Give the children of a group their own clock, run them faster with the group speed, and start them with the group." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/composition-groups-20bae2/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Composition Groups](https://img.ly/docs/cesdk/mac-catalyst/animation/composition-groups-20bae2/) --- ```swift file=@cesdk_swift_examples/engine-guides-composition-groups/CompositionGroups.swift reference-only import Foundation import IMGLYEngine struct CompositionGroupsGuideResult { let normalIsComposition: Bool let fastSpeed: Float let plainIsComposition: Bool let errorCode: EngineErrorCode? let pngData: Data } @MainActor func compositionGroups(engine: Engine) async throws -> CompositionGroupsGuideResult { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) try engine.block.setDuration(page, duration: 6) // Creates a lane: a group with a line and a ball that crosses the page in // 4 seconds of its own time func createLane(y: Float, color: Color) throws -> DesignBlockID { let line = try engine.block.create(.graphic) try engine.block.setShape(line, shape: engine.block.createShape(.rect)) let lineFill = try engine.block.createFill(.color) try engine.block.setColor(lineFill, property: "fill/color/value", color: .rgba(r: 0.85, g: 0.85, b: 0.85, a: 1)) try engine.block.setFill(line, fill: lineFill) try engine.block.setWidth(line, value: 1680) try engine.block.setHeight(line, value: 6) try engine.block.setPositionX(line, value: 120) try engine.block.setPositionY(line, value: y + 37) try engine.block.appendChild(to: page, child: line) // Long enough for every lane clock, so the line stays visible try engine.block.setDuration(line, duration: 12) let ball = try engine.block.create(.graphic) try engine.block.setShape(ball, shape: engine.block.createShape(.ellipse)) let ballFill = try engine.block.createFill(.color) try engine.block.setColor(ballFill, property: "fill/color/value", color: color) try engine.block.setFill(ball, fill: ballFill) try engine.block.setWidth(ball, value: 80) try engine.block.setHeight(ball, value: 80) try engine.block.setPositionY(ball, value: y) try engine.block.appendChild(to: page, child: ball) try engine.block.setDuration(ball, duration: 4) try engine.block.setKeyframes(ball, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(120)), Keyframe(time: 1, value: .float(1720)), ]) let lane = try engine.block.group([line, ball]) try engine.block.setDuration(lane, duration: 6) return lane } // The first lane is a composition group. Its children run on the group's // own clock, which starts when the group starts on the page. let normal = try createLane(y: 150, color: .rgba(r: 0.6, g: 0.6, b: 0.6, a: 1)) try engine.block.setBool(normal, property: "group/composition", value: true) let normalIsComposition = try engine.block.getBool(normal, property: "group/composition") print(normalIsComposition) // true // At speed 2, the children see 2 seconds for every second of the page let fast = try createLane(y: 400, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) try engine.block.setBool(fast, property: "group/composition", value: true) try engine.block.setPlaybackSpeed(fast, speed: 2) let fastSpeed = try engine.block.getPlaybackSpeed(fast) print(fastSpeed) // 2.0 // The group starts 1 second into the page, and its children start with it let late = try createLane(y: 650, color: .rgba(r: 0.9, g: 0.3, b: 0.4, a: 1)) try engine.block.setBool(late, property: "group/composition", value: true) try engine.block.setTimeOffset(late, offset: 1) try engine.block.setDuration(late, duration: 5) // Without composition, the speed of a group has no effect let plain = try createLane(y: 900, color: .rgba(r: 0.2, g: 0.7, b: 0.3, a: 1)) try engine.block.setPlaybackSpeed(plain, speed: 2) let plainIsComposition = try engine.block.getBool(plain, property: "group/composition") print(plainIsComposition) // false var errorCode: EngineErrorCode? do { // A group runs at 0.25 times the page speed or faster try engine.block.setPlaybackSpeed(normal, speed: 0.1) } catch let error as EngineError { errorCode = error.catalogCode print(errorCode == .editorPlaybackSpeedTooLow) // true } // Export the frame at 1.5 seconds try engine.block.setPlaybackTime(page, time: 1.5) let pngData = try await engine.block.export(page, mimeType: .png) print(!pngData.isEmpty) // true try await engine.captureGuide(data: pngData, label: "hero", mimeType: .png) return CompositionGroupsGuideResult( normalIsComposition: normalIsComposition, fastSpeed: fastSpeed, plainIsComposition: plainIsComposition, errorCode: errorCode, pngData: pngData, ) } ``` Give the children of a group their own clock, run them faster with the group speed, and start them with the group. ![Four lanes of the same moving ball: normal speed, double speed, a later start and a plain group, at 1.5 seconds](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-composition-groups) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. A composition group runs its children on its own clock, like a precomposition in a motion design tool. The clock starts when the group starts on the page, and the group's speed runs it faster or slower. The editor has no composition control, so you set it with the block API. This guide covers how to turn composition on, how to run the children faster, how to start them with the group, what a plain group does, and the error to expect. It ends with a frame export. ## Creating the Lanes Each lane of the example is a group with a gray line and a ball on `page`, the 6-second page of the scene. The ball has a `position/x` track and crosses the page in 4 seconds of its own time. `group(_:)` groups the two blocks. ```swift highlight-compositionGroups-createLane // Creates a lane: a group with a line and a ball that crosses the page in // 4 seconds of its own time func createLane(y: Float, color: Color) throws -> DesignBlockID { let line = try engine.block.create(.graphic) try engine.block.setShape(line, shape: engine.block.createShape(.rect)) let lineFill = try engine.block.createFill(.color) try engine.block.setColor(lineFill, property: "fill/color/value", color: .rgba(r: 0.85, g: 0.85, b: 0.85, a: 1)) try engine.block.setFill(line, fill: lineFill) try engine.block.setWidth(line, value: 1680) try engine.block.setHeight(line, value: 6) try engine.block.setPositionX(line, value: 120) try engine.block.setPositionY(line, value: y + 37) try engine.block.appendChild(to: page, child: line) // Long enough for every lane clock, so the line stays visible try engine.block.setDuration(line, duration: 12) let ball = try engine.block.create(.graphic) try engine.block.setShape(ball, shape: engine.block.createShape(.ellipse)) let ballFill = try engine.block.createFill(.color) try engine.block.setColor(ballFill, property: "fill/color/value", color: color) try engine.block.setFill(ball, fill: ballFill) try engine.block.setWidth(ball, value: 80) try engine.block.setHeight(ball, value: 80) try engine.block.setPositionY(ball, value: y) try engine.block.appendChild(to: page, child: ball) try engine.block.setDuration(ball, duration: 4) try engine.block.setKeyframes(ball, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(120)), Keyframe(time: 1, value: .float(1720)), ]) let lane = try engine.block.group([line, ball]) try engine.block.setDuration(lane, duration: 6) return lane } ``` ## Turning Composition On `group/composition` is a bool property of a group and is off by default. `setBool(_:property:value:)` turns it on, and `getBool(_:property:)` reads it. The children of the first lane now run on the group's clock, which starts when the group starts on the page. ```swift highlight-compositionGroups-composition // The first lane is a composition group. Its children run on the group's // own clock, which starts when the group starts on the page. let normal = try createLane(y: 150, color: .rgba(r: 0.6, g: 0.6, b: 0.6, a: 1)) try engine.block.setBool(normal, property: "group/composition", value: true) let normalIsComposition = try engine.block.getBool(normal, property: "group/composition") print(normalIsComposition) // true ``` ## Running the Children Faster `setPlaybackSpeed(_:speed:)` on a composition group runs its children that many times faster. The speed of a group has no upper bound, and `getPlaybackSpeed(_:)` reads it back. At speed 2, the blue ball crosses the page in 2 seconds. Its duration of 4 seconds is measured on the composition clock, so the ball disappears after 2 seconds of the page. ```swift highlight-compositionGroups-speed // At speed 2, the children see 2 seconds for every second of the page let fast = try createLane(y: 400, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) try engine.block.setBool(fast, property: "group/composition", value: true) try engine.block.setPlaybackSpeed(fast, speed: 2) let fastSpeed = try engine.block.getPlaybackSpeed(fast) print(fastSpeed) // 2.0 ``` ## Starting the Children With the Group The time offsets of the children are measured on the composition clock, which starts with the group. `setTimeOffset(_:offset:)` starts the third group 1 second into the page, so its ball starts 1 second later than the others. ```swift highlight-compositionGroups-offset // The group starts 1 second into the page, and its children start with it let late = try createLane(y: 650, color: .rgba(r: 0.9, g: 0.3, b: 0.4, a: 1)) try engine.block.setBool(late, property: "group/composition", value: true) try engine.block.setTimeOffset(late, offset: 1) try engine.block.setDuration(late, duration: 5) ``` ## A Plain Group A group without composition keeps its children on the page clock and ignores its speed. The green ball crosses the page at the same pace as the gray one, although its group has speed 2. ```swift highlight-compositionGroups-plainGroup // Without composition, the speed of a group has no effect let plain = try createLane(y: 900, color: .rgba(r: 0.2, g: 0.7, b: 0.3, a: 1)) try engine.block.setPlaybackSpeed(plain, speed: 2) let plainIsComposition = try engine.block.getBool(plain, property: "group/composition") print(plainIsComposition) // false ``` ## Handling Errors A group runs at 0.25 times the page speed or faster. A lower speed throws an `EngineError` whose `catalogCode` names the cause, and the speed stays unchanged. ```swift highlight-compositionGroups-errors var errorCode: EngineErrorCode? do { // A group runs at 0.25 times the page speed or faster try engine.block.setPlaybackSpeed(normal, speed: 0.1) } catch let error as EngineError { errorCode = error.catalogCode print(errorCode == .editorPlaybackSpeedTooLow) // true } ``` ## Exporting a Frame We export the page as PNG at 1.5 seconds. The gray and green balls are at 37.5 percent of their lanes, the blue ball at 75 percent, and the red ball at 12.5 percent, because its group started 1 second later. ```swift highlight-compositionGroups-export // Export the frame at 1.5 seconds try engine.block.setPlaybackTime(page, time: 1.5) let pngData = try await engine.block.export(page, mimeType: .png) print(!pngData.isEmpty) // true ``` ## Composition Groups and Other Block Operations - Nested composition groups multiply their speeds. - A child's duration is not clamped to the group duration. A child is hidden after its end on the composition clock. - `playback/timeRemap` with `playback/timeRemapEnabled` replaces the group's local time with a value or a keyframe track, and the speed still multiplies it. - Ungrouping a composition group converts the time offset and duration of each child to the page clock, so the children keep playing at the same moments. - Saving and loading keep `group/composition` and the speed, and undo and redo restore them. - The HTML5 export does not export the composition clock, so the children play at page time there. ## Troubleshooting - **The speed is too low** (`EngineErrorCode.editorPlaybackSpeedTooLow`): use a speed of 0.25 or more. - **The speed is not a number** (`EngineErrorCode.blockValueNotFinite`): pass a finite speed. - **The speed has no effect**: turn on `group/composition` for the group. - **A child disappears early**: its duration is measured on the composition clock, so a faster group ends it sooner on the page. Make the child longer. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.create(_:)` | Create a graphic block | | `engine.block.createShape(_:)` | Create a rect or ellipse shape | | `engine.block.setShape(_:shape:)` | Give a graphic its shape | | `engine.block.createFill(_:)` | Create a color fill | | `engine.block.setColor(_:property:color:)` | Set the fill color | | `engine.block.setFill(_:fill:)` | Give a graphic its fill | | `engine.block.setWidth(_:value:)` | Set the width of a block | | `engine.block.setHeight(_:value:)` | Set the height of a block | | `engine.block.setPositionX(_:value:)` | Set the x position of a block | | `engine.block.setPositionY(_:value:)` | Set the y position of a block | | `engine.block.appendChild(to:child:)` | Add a block to the page | | `engine.block.setDuration(_:duration:)` | Set the duration of a block or group | | `engine.block.setKeyframes(_:property:keyframes:mode:)` | Move the ball | | `engine.block.group(_:)` | Group blocks into a group | | `engine.block.setBool(_:property:value:)` | Turn composition on or off | | `engine.block.getBool(_:property:)` | Check whether a group is a composition group | | `engine.block.setPlaybackSpeed(_:speed:)` | Set the speed of a group, 0.25 or more | | `engine.block.getPlaybackSpeed(_:)` | Read the speed of a group | | `engine.block.setTimeOffset(_:offset:)` | Start a group later on the page | | `engine.block.setPlaybackTime(_:time:)` | Seek before you export a frame | | `engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:)` | Export the frame as PNG | ### Properties | Property | Type | Description | | --- | --- | --- | | `group/composition` | Bool | Run the children of a group on the group's own clock. Default off | | `playback/speed` | Float | Speed of the composition clock, 0.25 or more. Default 1 | ## Next Steps - [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/): animate the children of a composition group. - [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/): add preset animations to blocks. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create Animations" description: "Build entrance, exit, loop, and text animations with CE.SDK on Apple platforms." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-animations/CreateAnimations.swift reference-only import Foundation import IMGLYEngine @MainActor func createAnimations(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(block, value: 100) try engine.block.setPositionY(block, value: 50) try engine.block.setWidth(block, value: 300) try engine.block.setHeight(block, value: 300) try engine.block.appendChild(to: page, child: block) let fill = try engine.block.createFill(.image) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(block, fill: fill) guard try engine.block.supportsAnimation(block) else { return } let slideIn = try engine.block.createAnimation(.slide) try engine.block.setInAnimation(block, animation: slideIn) try engine.block.setDuration(slideIn, duration: 1.2) try engine.block.setEnum(slideIn, property: "animationEasing", value: "EaseOut") try engine.block.setFloat(slideIn, property: "animation/slide/direction", value: 1.5 * .pi) let fadeOut = try engine.block.createAnimation(.fade) try engine.block.setOutAnimation(block, animation: fadeOut) try engine.block.setDuration(fadeOut, duration: 1.0) try engine.block.setEnum(fadeOut, property: "animationEasing", value: "EaseIn") let pulsatingLoop = try engine.block.createAnimation(.pulsatingLoop) try engine.block.setLoopAnimation(block, animation: pulsatingLoop) try engine.block.setDuration(pulsatingLoop, duration: 1.5) let slideProperties = try engine.block.findAllProperties(slideIn) print("Slide animation properties: \(slideProperties)") let easingOptions = try engine.block.getEnumValues(ofProperty: "animationEasing") print("Available easing options: \(easingOptions)") let text = try engine.block.create(.text) try engine.block.setPositionX(text, value: 100) try engine.block.setPositionY(text, value: 400) try engine.block.setWidth(text, value: 600) try engine.block.setHeight(text, value: 100) try engine.block.replaceText(text, text: "Entrance • Exit • Loop") try engine.block.appendChild(to: page, child: text) let textAnimation = try engine.block.createAnimation(.fade) try engine.block.setInAnimation(text, animation: textAnimation) try engine.block.setDuration(textAnimation, duration: 1.5) try engine.block.setEnum(textAnimation, property: "textAnimationWritingStyle", value: "Word") try engine.block.setFloat(textAnimation, property: "textAnimationOverlap", value: 0.3) let currentIn = try engine.block.getInAnimation(block) let currentOut = try engine.block.getOutAnimation(block) let currentLoop = try engine.block.getLoopAnimation(block) print("Animation IDs — In: \(currentIn), Out: \(currentOut), Loop: \(currentLoop)") if engine.block.isValid(currentIn) { try engine.block.destroy(currentIn) let zoomIn = try engine.block.createAnimation(.zoom) try engine.block.setInAnimation(block, animation: zoomIn) try engine.block.setDuration(zoomIn, duration: 0.8) } let currentAnimation = try engine.block.getInAnimation(block) if engine.block.isValid(currentAnimation) { try engine.block.destroy(currentAnimation) } let newAnimation = try engine.block.createAnimation(.fade) try engine.block.setInAnimation(block, animation: newAnimation) } ``` Add motion to design elements by creating entrance, exit, and loop animations using CE.SDK's animation system. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-animations) CE.SDK provides a unified animation system for adding motion to design elements. Animations are created as separate block instances and attached to target blocks using type-specific methods. You can apply entrance animations (how blocks appear), exit animations (how blocks leave), and loop animations (continuous motion while visible). Text blocks support additional properties for word-by-word or character-by-character reveals. This guide covers how to create and configure animations programmatically, including entrance, exit, loop, and text animations with customizable timing and easing. Use this page as the complete end-to-end workflow. The focused guides linked below cover base, text, editing, and animation-type details separately. ## Animation Fundamentals Verify that a block supports animations before creating and attaching them. The basic pattern creates an animation instance with `createAnimation(_:)`, attaches it with the appropriate setter, and configures the duration with `setDuration(_:duration:)`. ```swift highlight-createAnimations-checkSupport guard try engine.block.supportsAnimation(block) else { return } let slideIn = try engine.block.createAnimation(.slide) try engine.block.setInAnimation(block, animation: slideIn) try engine.block.setDuration(slideIn, duration: 1.2) ``` Animation support is available for: - **Graphic blocks** with image or video fills - **Text blocks** with additional writing style options - **Shape blocks** with fills CE.SDK provides several animation presets: - **Entrance animations**: `slide`, `fade`, `blur`, `zoom`, `pop`, `wipe`, `pan` - **Exit animations**: same types as entrance - **Loop animations**: `breathingLoop`, `spinLoop`, `fadeLoop`, `pulsatingLoop`, `jumpLoop`, `squeezeLoop`, `swayLoop` ## Entrance Animations Entrance animations define how blocks appear on screen. Attach them with `setInAnimation(_:animation:)`. Configure the curve with the `animationEasing` property and, for `slide`, the `animation/slide/direction` property in radians. ```swift highlight-createAnimations-entranceAnimation try engine.block.setEnum(slideIn, property: "animationEasing", value: "EaseOut") try engine.block.setFloat(slideIn, property: "animation/slide/direction", value: 1.5 * .pi) ``` The `animationEasing` property accepts `Linear`, `EaseIn`, `EaseOut`, `EaseInOut`, and higher-order curves like `EaseOutQuint` and `EaseOutBack`. Call `getEnumValues(ofProperty: "animationEasing")` to enumerate the full list at runtime. Slide direction is the direction of travel in radians: `0` moves right, `0.5 * .pi` moves down, `.pi` moves left, and `1.5 * .pi` moves up. The snippet above moves the block up, so it enters from the bottom. ## Exit Animations Exit animations define how blocks leave the screen. Attach them with `setOutAnimation(_:animation:)`. CE.SDK manages timing automatically to prevent overlap between entrance and exit animations. ```swift highlight-createAnimations-exitAnimation let fadeOut = try engine.block.createAnimation(.fade) try engine.block.setOutAnimation(block, animation: fadeOut) try engine.block.setDuration(fadeOut, duration: 1.0) try engine.block.setEnum(fadeOut, property: "animationEasing", value: "EaseIn") ``` When a block has both entrance and exit animations, CE.SDK adjusts their timing based on the block's duration in the composition. ## Loop Animations Loop animations run continuously while the block is visible. Use animation types ending in `Loop` and attach them with `setLoopAnimation(_:animation:)`. ```swift highlight-createAnimations-loopAnimation let pulsatingLoop = try engine.block.createAnimation(.pulsatingLoop) try engine.block.setLoopAnimation(block, animation: pulsatingLoop) try engine.block.setDuration(pulsatingLoop, duration: 1.5) ``` Loop animations continue throughout the block's visible duration, creating continuous motion effects like breathing, spinning, or pulsating. ## Animation Properties Each animation type exposes configurable properties. Use `setFloat(_:property:value:)` and `setEnum(_:property:value:)` to adjust them, and `findAllProperties(_:)` to discover available options. To enumerate the allowed values for an enum property, call `getEnumValues(ofProperty:)`. ```swift highlight-createAnimations-animationProperties let slideProperties = try engine.block.findAllProperties(slideIn) print("Slide animation properties: \(slideProperties)") let easingOptions = try engine.block.getEnumValues(ofProperty: "animationEasing") print("Available easing options: \(easingOptions)") ``` Common configurable properties include: - **Direction**: The direction of travel in radians for slide animations (`0` moves right, `0.5 * .pi` down, `.pi` left, `1.5 * .pi` up). An entrance starts on the opposite side. - **Easing**: `Linear`, `EaseIn`, `EaseOut`, `EaseInOut` ## Text Animations Text blocks support additional animation properties for granular control over how text appears. The `textAnimationWritingStyle` property controls whether the animation applies to the entire text, line by line, word by word, or character by character. ```swift highlight-createAnimations-textAnimation let text = try engine.block.create(.text) try engine.block.setPositionX(text, value: 100) try engine.block.setPositionY(text, value: 400) try engine.block.setWidth(text, value: 600) try engine.block.setHeight(text, value: 100) try engine.block.replaceText(text, text: "Entrance • Exit • Loop") try engine.block.appendChild(to: page, child: text) let textAnimation = try engine.block.createAnimation(.fade) try engine.block.setInAnimation(text, animation: textAnimation) try engine.block.setDuration(textAnimation, duration: 1.5) try engine.block.setEnum(textAnimation, property: "textAnimationWritingStyle", value: "Word") try engine.block.setFloat(textAnimation, property: "textAnimationOverlap", value: 0.3) ``` Writing style options: - **`Line`**: Animate entire lines together - **`Word`**: Animate word by word - **`Character`**: Animate character by character The `textAnimationOverlap` property (`0` to `1`) controls the cascading effect. A value of `0` means sequential animation, while values closer to `1` create more overlap between segments. ## Managing Animation Lifecycle Retrieve current animations with `getInAnimation(_:)`, `getOutAnimation(_:)`, and `getLoopAnimation(_:)`. An empty slot returns an invalid `DesignBlockID`; use `isValid(_:)` to detect it before calling other APIs on the handle. When replacing an animation, destroy the old instance with `destroy(_:)` to prevent memory leaks. ```swift highlight-createAnimations-manageLifecycle let currentIn = try engine.block.getInAnimation(block) let currentOut = try engine.block.getOutAnimation(block) let currentLoop = try engine.block.getLoopAnimation(block) print("Animation IDs — In: \(currentIn), Out: \(currentOut), Loop: \(currentLoop)") if engine.block.isValid(currentIn) { try engine.block.destroy(currentIn) let zoomIn = try engine.block.createAnimation(.zoom) try engine.block.setInAnimation(block, animation: zoomIn) try engine.block.setDuration(zoomIn, duration: 0.8) } ``` ## Troubleshooting ### Animation Not Playing Verify the block supports animations with `supportsAnimation(_:)`. Ensure playback is active on the page. ### Duration Issues Set the duration on the animation instance, not on the target block. Attaching an entrance or exit animation first lets CE.SDK clamp its duration against the target block's visible duration and the opposing animation. ### Memory Leaks When replacing an animation, destroy the old animation instance before creating a new one: ```swift highlight-createAnimations-replaceMemoryLeaks let currentAnimation = try engine.block.getInAnimation(block) if engine.block.isValid(currentAnimation) { try engine.block.destroy(currentAnimation) } let newAnimation = try engine.block.createAnimation(.fade) try engine.block.setInAnimation(block, animation: newAnimation) ``` ### Timing Conflicts If entrance and exit animations seem to overlap incorrectly, CE.SDK automatically adjusts durations to prevent conflicts. Reduce individual animation durations if needed. ## API Reference | Method | Description | | --------------------------------------------- | ------------------------------------------- | | `engine.block.createAnimation(_:)` | Create animation instance | | `engine.block.supportsAnimation(_:)` | Check if block supports animations | | `engine.block.setInAnimation(_:animation:)` | Attach entrance animation | | `engine.block.setOutAnimation(_:animation:)` | Attach exit animation | | `engine.block.setLoopAnimation(_:animation:)` | Attach loop animation | | `engine.block.getInAnimation(_:)` | Get entrance animation (invalid ID if none) | | `engine.block.getOutAnimation(_:)` | Get exit animation (invalid ID if none) | | `engine.block.getLoopAnimation(_:)` | Get loop animation (invalid ID if none) | | `engine.block.setDuration(_:duration:)` | Set animation duration | | `engine.block.getDuration(_:)` | Get animation duration | | `engine.block.setEnum(_:property:value:)` | Set enum property (easing, writing style) | | `engine.block.setFloat(_:property:value:)` | Set float property (direction, overlap) | | `engine.block.findAllProperties(_:)` | List available properties | | `engine.block.getEnumValues(ofProperty:)` | Get enum options | | `engine.block.destroy(_:)` | Destroy animation instance | ## Next Steps - [Base Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/base-0fc5c4/) — Create and attach entrance, exit, and loop presets to non-text blocks. - [Text Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/text-d6f4aa/) — Configure text-specific presets, writing styles, and overlap. - [Edit Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/edit-32c12a/) — Inspect, update, replace, or remove an attached animation. - [Supported Animation Types](https://img.ly/docs/cesdk/mac-catalyst/animation/types-4e5f41/) — Compare available presets and their properties. - [Animation Overview](https://img.ly/docs/cesdk/mac-catalyst/animation/overview-6a2ef2/) — Review the animation model and core concepts. --- ## Related Pages - [Base Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/base-0fc5c4/) - Apply entrance, exit, and loop animation presets with duration, easing, and type-specific properties. - [Text Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/text-d6f4aa/) - Animate text elements with effects like fade, typewriter, and bounce for dynamic visual presentation. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Base Animations" description: "Apply entrance, exit, and loop animation presets with duration, easing, and type-specific properties." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/create/base-0fc5c4/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/) > [Base Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/base-0fc5c4/) --- ```swift file=@cesdk_swift_examples/engine-guides-base-animations/BaseAnimations.swift reference-only import Foundation import IMGLYEngine @MainActor func baseAnimations(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) let baseURL = try engine.guidesBaseURL let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(block, value: 100) try engine.block.setPositionY(block, value: 50) try engine.block.setWidth(block, value: 300) try engine.block.setHeight(block, value: 300) try engine.block.appendChild(to: page, child: block) let fill = try engine.block.createFill(.image) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(block, fill: fill) guard try engine.block.supportsAnimation(block) else { return } let slideIn = try engine.block.createAnimation(.slide) try engine.block.setInAnimation(block, animation: slideIn) try engine.block.setDuration(slideIn, duration: 1.0) let fadeIn = try engine.block.createAnimation(.fade) try engine.block.destroy(engine.block.getInAnimation(block)) try engine.block.setInAnimation(block, animation: fadeIn) try engine.block.setDuration(fadeIn, duration: 0.8) try engine.block.setEnum(fadeIn, property: "animationEasing", value: "EaseOut") let fadeOut = try engine.block.createAnimation(.fade) try engine.block.setOutAnimation(block, animation: fadeOut) try engine.block.setDuration(fadeOut, duration: 0.6) let breathing = try engine.block.createAnimation(.breathingLoop) try engine.block.setLoopAnimation(block, animation: breathing) try engine.block.setDuration(breathing, duration: 2.0) let slideFromTop = try engine.block.createAnimation(.slide) let slideProperties = try engine.block.findAllProperties(slideFromTop) print("Slide animation properties: \(slideProperties)") try engine.block.setFloat(slideFromTop, property: "animation/slide/direction", value: 0.5 * .pi) let currentIn = try engine.block.getInAnimation(block) let currentLoop = try engine.block.getLoopAnimation(block) let currentOut = try engine.block.getOutAnimation(block) print("Animation IDs — In: \(currentIn), Loop: \(currentLoop), Out: \(currentOut)") if engine.block.isValid(currentLoop) { try engine.block.destroy(currentLoop) } let squeeze = try engine.block.createAnimation(.squeezeLoop) try engine.block.setLoopAnimation(block, animation: squeeze) // Destroying a design block also destroys all its attached animations: // try engine.block.destroy(block) let easingOptions = try engine.block.getEnumValues(ofProperty: "animationEasing") print("Available easing options: \(easingOptions)") try engine.block.setEnum(fadeIn, property: "animationEasing", value: "EaseInOut") try engine.block.destroy(slideFromTop) } ``` Add motion to design blocks with entrance, exit, and loop animations using CE.SDK's animation system. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-base-animations) Base animations in CE.SDK add motion to design blocks through entrance (In), exit (Out), and loop animations. Animations are created as separate objects and attached to blocks, enabling reusable configurations across multiple elements. This guide covers creating animations, attaching them to blocks, configuring properties like duration and easing, and managing animation lifecycle. ## Animation Fundamentals Before applying animations to a block, verify it supports them using `supportsAnimation`. Once confirmed, create an animation instance with `createAnimation`, attach it with `setInAnimation`, and set its length with `setDuration`. ```swift highlight-baseAnim-supports guard try engine.block.supportsAnimation(block) else { return } let slideIn = try engine.block.createAnimation(.slide) try engine.block.setInAnimation(block, animation: slideIn) try engine.block.setDuration(slideIn, duration: 1.0) ``` CE.SDK provides several animation types via the `AnimationType` enum: - **Entrance animations:** `.slide`, `.fade`, `.blur`, `.grow`, `.zoom`, `.pop`, `.wipe`, `.pan`, `.baseline`, `.spin` - **Loop animations:** `.spinLoop`, `.fadeLoop`, `.blurLoop`, `.pulsatingLoop`, `.breathingLoop`, `.jumpLoop`, `.squeezeLoop`, `.swayLoop` ## Entrance Animations Entrance animations (In animations) define how a block appears on screen. Create the animation, attach it with `setInAnimation`, and configure its properties. When replacing an existing entrance animation, destroy the previous one with `destroy(getInAnimation(block))` before calling `setInAnimation` again — otherwise the old animation object leaks (see [Managing Animation Lifecycle](https://img.ly/docs/cesdk/mac-catalyst/animation/create/base-0fc5c4/#managing-animation-lifecycle)). ```swift highlight-baseAnim-entrance let fadeIn = try engine.block.createAnimation(.fade) try engine.block.destroy(engine.block.getInAnimation(block)) try engine.block.setInAnimation(block, animation: fadeIn) try engine.block.setDuration(fadeIn, duration: 0.8) try engine.block.setEnum(fadeIn, property: "animationEasing", value: "EaseOut") ``` `setEnum` configures the easing function. Available options include `"Linear"`, `"EaseIn"`, `"EaseOut"`, and `"EaseInOut"`. The `"EaseOut"` easing starts fast and slows down toward the end, creating a natural deceleration effect. ## Exit Animations Exit animations (Out animations) define how a block leaves the screen. Use `setOutAnimation` to attach them. ```swift highlight-baseAnim-exit let fadeOut = try engine.block.createAnimation(.fade) try engine.block.setOutAnimation(block, animation: fadeOut) try engine.block.setDuration(fadeOut, duration: 0.6) ``` When using both entrance and exit animations, CE.SDK automatically manages their timing to prevent overlap. Changing the duration of an In animation may adjust the Out animation's duration to maintain valid timing. ## Loop Animations Loop animations run continuously while the block is visible. Use `setLoopAnimation` to attach them. ```swift highlight-baseAnim-loop let breathing = try engine.block.createAnimation(.breathingLoop) try engine.block.setLoopAnimation(block, animation: breathing) try engine.block.setDuration(breathing, duration: 2.0) ``` The duration for loop animations defines the length of each cycle. A 2-second breathing loop completes one full pulse every 2 seconds. ## Animation Properties Each animation type has specific configurable properties. Use `findAllProperties` to discover available properties for an animation, and `setFloat` or `setEnum` to modify them. ```swift highlight-baseAnim-properties let slideFromTop = try engine.block.createAnimation(.slide) let slideProperties = try engine.block.findAllProperties(slideFromTop) print("Slide animation properties: \(slideProperties)") try engine.block.setFloat(slideFromTop, property: "animation/slide/direction", value: 0.5 * .pi) ``` For slide animations, the `animation/slide/direction` property is the angle in radians that the block travels along during entrance — the block starts off-screen on the opposite side and slides in: - `0` — Slides right (enters from the left) - `0.5 * .pi` — Slides down (enters from the top) - `.pi` — Slides left (enters from the right) - `1.5 * .pi` — Slides up (enters from the bottom) ## Managing Animation Lifecycle Animation objects must be properly managed to avoid memory leaks. When replacing an animation, destroy the old one before setting the new one. Retrieve current animations using `getInAnimation`, `getOutAnimation`, and `getLoopAnimation`. ```swift highlight-baseAnim-manage let currentIn = try engine.block.getInAnimation(block) let currentLoop = try engine.block.getLoopAnimation(block) let currentOut = try engine.block.getOutAnimation(block) print("Animation IDs — In: \(currentIn), Loop: \(currentLoop), Out: \(currentOut)") if engine.block.isValid(currentLoop) { try engine.block.destroy(currentLoop) } let squeeze = try engine.block.createAnimation(.squeezeLoop) try engine.block.setLoopAnimation(block, animation: squeeze) // Destroying a design block also destroys all its attached animations: // try engine.block.destroy(block) ``` These getters return an invalid `DesignBlockID` when no animation is attached. Use `engine.block.isValid(_:)` to check for that case — it reports `false` for the null sentinel that the getters return in the empty slot. Destroying a design block also destroys all of its attached animations, but detached animations must be destroyed manually. ## Easing Functions Query available easing options using `getEnumValues(ofProperty:)`. ```swift highlight-baseAnim-easing let easingOptions = try engine.block.getEnumValues(ofProperty: "animationEasing") print("Available easing options: \(easingOptions)") try engine.block.setEnum(fadeIn, property: "animationEasing", value: "EaseInOut") ``` Easing functions control animation acceleration: | Easing | Description | | ----------- | --------------------------------------------- | | `Linear` | Constant speed throughout | | `EaseIn` | Starts slow, accelerates toward the end | | `EaseOut` | Starts fast, decelerates toward the end | | `EaseInOut` | Starts slow, speeds up, then slows down again | ## Troubleshooting ### Animation Not Playing Verify that the target block supports animations, is visible during page playback, and has enough visible time for the animation duration. ### Duration Issues Set the duration on the animation instance, not on the target design block. Attaching an entrance or exit animation first lets CE.SDK clamp its duration against the target block's visible duration and the opposing animation. ## API Reference | Method | Description | | --------------------------------------------- | ----------------------------------------- | | `engine.block.createAnimation(_:)` | Create a new animation instance | | `engine.block.supportsAnimation(_:)` | Check if a block supports animations | | `engine.block.setInAnimation(_:animation:)` | Apply entrance animation to a block | | `engine.block.setOutAnimation(_:animation:)` | Apply exit animation to a block | | `engine.block.setLoopAnimation(_:animation:)` | Apply loop animation to a block | | `engine.block.getInAnimation(_:)` | Get the entrance animation ID | | `engine.block.getOutAnimation(_:)` | Get the exit animation ID | | `engine.block.getLoopAnimation(_:)` | Get the loop animation ID | | `engine.block.setDuration(_:duration:)` | Set animation duration in seconds | | `engine.block.getDuration(_:)` | Get animation duration | | `engine.block.setEnum(_:property:value:)` | Set an enum property (easing, etc.) | | `engine.block.setFloat(_:property:value:)` | Set a float property (direction, etc.) | | `engine.block.findAllProperties(_:)` | Get all configurable properties | | `engine.block.getEnumValues(ofProperty:)` | Get available values for an enum property | | `engine.block.destroy(_:)` | Destroy an animation instance | ## Next Steps - [Supported Animation Types](https://img.ly/docs/cesdk/mac-catalyst/animation/types-4e5f41/) — Explore the animation types available in CE.SDK and their configurable properties - [Text Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/text-d6f4aa/) — Animate text with writing styles and character-level control - [Animation Overview](https://img.ly/docs/cesdk/mac-catalyst/animation/overview-6a2ef2/) — Understand animation concepts and capabilities - [Edit Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/edit-32c12a/) — Modify existing animations on blocks --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Text Animations" description: "Animate text elements with effects like fade, typewriter, and bounce for dynamic visual presentation." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/create/text-d6f4aa/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/) > [Text Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/text-d6f4aa/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-animations/TextAnimations.swift reference-only import Foundation import IMGLYEngine @MainActor func textAnimations(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.setDuration(page, duration: 10) try engine.block.appendChild(to: scene, child: page) let text1 = try engine.block.create(.text) try engine.block.setPositionX(text1, value: 100) try engine.block.setPositionY(text1, value: 100) try engine.block.setWidth(text1, value: 600) try engine.block.setHeight(text1, value: 200) try engine.block.replaceText(text1, text: "Creating\nText\nAnimations") try engine.block.appendChild(to: page, child: text1) let baselineAnimation = try engine.block.createAnimation(.baseline) try engine.block.setInAnimation(text1, animation: baselineAnimation) try engine.block.setDuration(baselineAnimation, duration: 2.0) let text2 = try engine.block.create(.text) try engine.block.setPositionX(text2, value: 700) try engine.block.setPositionY(text2, value: 100) try engine.block.setWidth(text2, value: 600) try engine.block.setHeight(text2, value: 200) try engine.block.replaceText(text2, text: "Line by line\nanimation\nfor text") try engine.block.appendChild(to: page, child: text2) let lineAnimation = try engine.block.createAnimation(.baseline) try engine.block.setInAnimation(text2, animation: lineAnimation) try engine.block.setDuration(lineAnimation, duration: 2.0) try engine.block.setEnum(lineAnimation, property: "textAnimationWritingStyle", value: "Line") try engine.block.setEnum(lineAnimation, property: "animationEasing", value: "EaseOut") let text3 = try engine.block.create(.text) try engine.block.setPositionX(text3, value: 1300) try engine.block.setPositionY(text3, value: 100) try engine.block.setWidth(text3, value: 600) try engine.block.setHeight(text3, value: 200) try engine.block.replaceText(text3, text: "Animate word by word for emphasis") try engine.block.appendChild(to: page, child: text3) let wordAnimation = try engine.block.createAnimation(.baseline) try engine.block.setInAnimation(text3, animation: wordAnimation) try engine.block.setDuration(wordAnimation, duration: 2.5) try engine.block.setEnum(wordAnimation, property: "textAnimationWritingStyle", value: "Word") try engine.block.setEnum(wordAnimation, property: "animationEasing", value: "EaseOut") let text4 = try engine.block.create(.text) try engine.block.setPositionX(text4, value: 100) try engine.block.setPositionY(text4, value: 400) try engine.block.setWidth(text4, value: 600) try engine.block.setHeight(text4, value: 200) try engine.block.replaceText(text4, text: "Character by character for typewriter effect") try engine.block.appendChild(to: page, child: text4) let characterAnimation = try engine.block.createAnimation(.baseline) try engine.block.setInAnimation(text4, animation: characterAnimation) try engine.block.setDuration(characterAnimation, duration: 3.0) try engine.block.setEnum(characterAnimation, property: "textAnimationWritingStyle", value: "Character") try engine.block.setEnum(characterAnimation, property: "animationEasing", value: "Linear") let text5 = try engine.block.create(.text) try engine.block.setPositionX(text5, value: 700) try engine.block.setPositionY(text5, value: 400) try engine.block.setWidth(text5, value: 600) try engine.block.setHeight(text5, value: 200) try engine.block.replaceText(text5, text: "Sequential animation with zero overlap") try engine.block.appendChild(to: page, child: text5) let sequentialAnimation = try engine.block.createAnimation(.pan) try engine.block.setInAnimation(text5, animation: sequentialAnimation) try engine.block.setDuration(sequentialAnimation, duration: 2.0) try engine.block.setEnum(sequentialAnimation, property: "textAnimationWritingStyle", value: "Word") try engine.block.setFloat(sequentialAnimation, property: "textAnimationOverlap", value: 0.0) try engine.block.setEnum(sequentialAnimation, property: "animationEasing", value: "EaseOut") let text6 = try engine.block.create(.text) try engine.block.setPositionX(text6, value: 1300) try engine.block.setPositionY(text6, value: 400) try engine.block.setWidth(text6, value: 600) try engine.block.setHeight(text6, value: 200) try engine.block.replaceText(text6, text: "Cascading animation with partial overlap") try engine.block.appendChild(to: page, child: text6) let cascadingAnimation = try engine.block.createAnimation(.pan) try engine.block.setInAnimation(text6, animation: cascadingAnimation) try engine.block.setDuration(cascadingAnimation, duration: 1.5) try engine.block.setEnum(cascadingAnimation, property: "textAnimationWritingStyle", value: "Word") try engine.block.setFloat(cascadingAnimation, property: "textAnimationOverlap", value: 0.4) try engine.block.setEnum(cascadingAnimation, property: "animationEasing", value: "EaseOut") let text7 = try engine.block.create(.text) try engine.block.setPositionX(text7, value: 100) try engine.block.setPositionY(text7, value: 700) try engine.block.setWidth(text7, value: 1200) try engine.block.setHeight(text7, value: 200) try engine.block.replaceText(text7, text: "Combine writing style, overlap, duration, and easing") try engine.block.appendChild(to: page, child: text7) let combinedAnimation = try engine.block.createAnimation(.fade) try engine.block.setInAnimation(text7, animation: combinedAnimation) try engine.block.setEnum(combinedAnimation, property: "textAnimationWritingStyle", value: "Word") try engine.block.setFloat(combinedAnimation, property: "textAnimationOverlap", value: 0.3) try engine.block.setDuration(combinedAnimation, duration: 1.5) try engine.block.setEnum(combinedAnimation, property: "animationEasing", value: "EaseInOut") let writingStyleOptions = try engine.block.getEnumValues(ofProperty: "textAnimationWritingStyle") let easingOptions = try engine.block.getEnumValues(ofProperty: "animationEasing") _ = writingStyleOptions _ = easingOptions } ``` Create engaging text animations that reveal content line by line, word by word, or character by character with granular control over timing and overlap. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-text-animations) Text animations in CE.SDK animate text blocks with granular control over how the text appears. Unlike standard block animations, text animations support writing styles that determine whether animation applies to the entire text, line by line, word by word, or character by character. This guide covers text-specific animation properties like writing styles and segment overlap, enabling dynamic and engaging text presentations in your designs. ## Text Animation Fundamentals Create animations by first creating an animation instance, then attaching it to a text block. The animation defines how the text animates, while the text block contains the content and styling. ```swift highlight-textAnimations-createAnimation let text1 = try engine.block.create(.text) try engine.block.setPositionX(text1, value: 100) try engine.block.setPositionY(text1, value: 100) try engine.block.setWidth(text1, value: 600) try engine.block.setHeight(text1, value: 200) try engine.block.replaceText(text1, text: "Creating\nText\nAnimations") try engine.block.appendChild(to: page, child: text1) let baselineAnimation = try engine.block.createAnimation(.baseline) try engine.block.setInAnimation(text1, animation: baselineAnimation) try engine.block.setDuration(baselineAnimation, duration: 2.0) ``` Animations are created with `createAnimation(_:)` using a type like `.baseline`, `.fade`, or `.pan`. Attach the animation to the text block's entrance with `setInAnimation(_:animation:)` and set the timing with `setDuration(_:duration:)`. ## Writing Style Control Text animations support different granularity levels through the `textAnimationWritingStyle` property. This controls whether the animation applies to the entire text at once, or breaks it into segments (lines, words, or characters). Query the available options with `getEnumValues(ofProperty: "textAnimationWritingStyle")`. ### Line-by-Line Animation The `Line` writing style animates text one line at a time from top to bottom. Each line appears sequentially, creating a structured reveal effect. ```swift highlight-textAnimations-writingStyleLine let text2 = try engine.block.create(.text) try engine.block.setPositionX(text2, value: 700) try engine.block.setPositionY(text2, value: 100) try engine.block.setWidth(text2, value: 600) try engine.block.setHeight(text2, value: 200) try engine.block.replaceText(text2, text: "Line by line\nanimation\nfor text") try engine.block.appendChild(to: page, child: text2) let lineAnimation = try engine.block.createAnimation(.baseline) try engine.block.setInAnimation(text2, animation: lineAnimation) try engine.block.setDuration(lineAnimation, duration: 2.0) try engine.block.setEnum(lineAnimation, property: "textAnimationWritingStyle", value: "Line") try engine.block.setEnum(lineAnimation, property: "animationEasing", value: "EaseOut") ``` Set the writing style to `"Line"` with `setEnum(_:property:value:)`. This is ideal for revealing multi-line text in a clear, organized manner. ### Word-by-Word Animation The `Word` writing style animates text one word at a time in reading order. This creates emphasis and draws attention to individual words. ```swift highlight-textAnimations-writingStyleWord let text3 = try engine.block.create(.text) try engine.block.setPositionX(text3, value: 1300) try engine.block.setPositionY(text3, value: 100) try engine.block.setWidth(text3, value: 600) try engine.block.setHeight(text3, value: 200) try engine.block.replaceText(text3, text: "Animate word by word for emphasis") try engine.block.appendChild(to: page, child: text3) let wordAnimation = try engine.block.createAnimation(.baseline) try engine.block.setInAnimation(text3, animation: wordAnimation) try engine.block.setDuration(wordAnimation, duration: 2.5) try engine.block.setEnum(wordAnimation, property: "textAnimationWritingStyle", value: "Word") try engine.block.setEnum(wordAnimation, property: "animationEasing", value: "EaseOut") ``` Setting the writing style to `"Word"` is perfect for dynamic, engaging text reveals that emphasize key phrases. ### Character-by-Character Animation The `Character` writing style animates text one character at a time, creating a classic typewriter effect. This is the most granular animation option. ```swift highlight-textAnimations-writingStyleCharacter let text4 = try engine.block.create(.text) try engine.block.setPositionX(text4, value: 100) try engine.block.setPositionY(text4, value: 400) try engine.block.setWidth(text4, value: 600) try engine.block.setHeight(text4, value: 200) try engine.block.replaceText(text4, text: "Character by character for typewriter effect") try engine.block.appendChild(to: page, child: text4) let characterAnimation = try engine.block.createAnimation(.baseline) try engine.block.setInAnimation(text4, animation: characterAnimation) try engine.block.setDuration(characterAnimation, duration: 3.0) try engine.block.setEnum(characterAnimation, property: "textAnimationWritingStyle", value: "Character") try engine.block.setEnum(characterAnimation, property: "animationEasing", value: "Linear") ``` The `"Character"` writing style is ideal for typewriter effects and when you want maximum control over the animation timing. ## Segment Overlap Configuration The `textAnimationOverlap` property controls timing between animation segments. A value of `0` means segments animate sequentially; values between `0` and `1` create cascading effects where segments overlap partially. Use `setFloat(_:property:value:)` to set the overlap value. ### Sequential Animation (Overlap = 0) When overlap is set to `0`, each segment completes before the next begins, creating a clear, structured reveal effect. ```swift highlight-textAnimations-overlapSequential let text5 = try engine.block.create(.text) try engine.block.setPositionX(text5, value: 700) try engine.block.setPositionY(text5, value: 400) try engine.block.setWidth(text5, value: 600) try engine.block.setHeight(text5, value: 200) try engine.block.replaceText(text5, text: "Sequential animation with zero overlap") try engine.block.appendChild(to: page, child: text5) let sequentialAnimation = try engine.block.createAnimation(.pan) try engine.block.setInAnimation(text5, animation: sequentialAnimation) try engine.block.setDuration(sequentialAnimation, duration: 2.0) try engine.block.setEnum(sequentialAnimation, property: "textAnimationWritingStyle", value: "Word") try engine.block.setFloat(sequentialAnimation, property: "textAnimationOverlap", value: 0.0) try engine.block.setEnum(sequentialAnimation, property: "animationEasing", value: "EaseOut") ``` Sequential animation ensures each text segment fully appears before the next one starts, making it perfect for emphasis and readability. ### Cascading Animation (Overlap = 0.4) When overlap is set to a value between `0` and `1`, segments animate in a cascading pattern, creating a smooth, flowing effect as they blend together. ```swift highlight-textAnimations-overlapCascading let text6 = try engine.block.create(.text) try engine.block.setPositionX(text6, value: 1300) try engine.block.setPositionY(text6, value: 400) try engine.block.setWidth(text6, value: 600) try engine.block.setHeight(text6, value: 200) try engine.block.replaceText(text6, text: "Cascading animation with partial overlap") try engine.block.appendChild(to: page, child: text6) let cascadingAnimation = try engine.block.createAnimation(.pan) try engine.block.setInAnimation(text6, animation: cascadingAnimation) try engine.block.setDuration(cascadingAnimation, duration: 1.5) try engine.block.setEnum(cascadingAnimation, property: "textAnimationWritingStyle", value: "Word") try engine.block.setFloat(cascadingAnimation, property: "textAnimationOverlap", value: 0.4) try engine.block.setEnum(cascadingAnimation, property: "animationEasing", value: "EaseOut") ``` Cascading animation with partial overlap creates fluid text reveals that feel natural and engaging. ## Combining with Animation Properties Text animations can be enhanced with standard animation properties like duration and easing. Duration controls the overall timing of the animation, while easing controls the acceleration curve. The snippet below applies all four knobs — writing style, overlap, duration, and easing — to a single animation, then queries the engine for available enum options. ```swift highlight-textAnimations-durationEasing let text7 = try engine.block.create(.text) try engine.block.setPositionX(text7, value: 100) try engine.block.setPositionY(text7, value: 700) try engine.block.setWidth(text7, value: 1200) try engine.block.setHeight(text7, value: 200) try engine.block.replaceText(text7, text: "Combine writing style, overlap, duration, and easing") try engine.block.appendChild(to: page, child: text7) let combinedAnimation = try engine.block.createAnimation(.fade) try engine.block.setInAnimation(text7, animation: combinedAnimation) try engine.block.setEnum(combinedAnimation, property: "textAnimationWritingStyle", value: "Word") try engine.block.setFloat(combinedAnimation, property: "textAnimationOverlap", value: 0.3) try engine.block.setDuration(combinedAnimation, duration: 1.5) try engine.block.setEnum(combinedAnimation, property: "animationEasing", value: "EaseInOut") let writingStyleOptions = try engine.block.getEnumValues(ofProperty: "textAnimationWritingStyle") let easingOptions = try engine.block.getEnumValues(ofProperty: "animationEasing") ``` Set the easing function with `setEnum(_:property: "animationEasing", value:)` using values such as `"EaseIn"`, `"EaseOut"`, `"EaseInOut"`, or `"Linear"`. Query available writing style and easing options with `getEnumValues(ofProperty:)`. Combining writing style, overlap, duration, and easing gives complete control over how text animates. ## API Reference | Method | Description | | --- | --- | | `engine.block.createAnimation(_:)` | Create a new animation instance | | `engine.block.setInAnimation(_:animation:)` | Apply animation to block entrance | | `engine.block.setLoopAnimation(_:animation:)` | Apply looping animation to block | | `engine.block.setOutAnimation(_:animation:)` | Apply animation to block exit | | `engine.block.setDuration(_:duration:)` | Set animation duration in seconds | | `engine.block.setEnum(_:property:value:)` | Set enum property (writing style, easing) | | `engine.block.setFloat(_:property:value:)` | Set float property (overlap value) | | `engine.block.getEnumValues(ofProperty:)` | Get available enum options for a property | | `engine.block.replaceText(_:text:)` | Set text content of a text block | | `engine.block.supportsAnimation(_:)` | Check if a block supports animations | ## Next Steps - [Supported Animation Types](https://img.ly/docs/cesdk/mac-catalyst/animation/types-4e5f41/) — Explore the animation types available in CE.SDK and their configurable properties - [Base Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/base-0fc5c4/) — Create entrance, exit, and loop animations - [Edit Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/edit-32c12a/) — Modify existing animations on blocks - [Animation Overview](https://img.ly/docs/cesdk/mac-catalyst/animation/overview-6a2ef2/) — Understand animation concepts and capabilities --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Edit Animations" description: "Modify existing animations in CE.SDK by reading properties, changing duration and easing, adjusting direction, and replacing or removing animations from blocks." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/edit-32c12a/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Edit Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/edit-32c12a/) --- ```swift file=@cesdk_swift_examples/engine-guides-edit-animations/EditAnimations.swift reference-only import Foundation import IMGLYEngine @MainActor func editAnimations(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(block, value: 100) try engine.block.setPositionY(block, value: 50) try engine.block.setWidth(block, value: 300) try engine.block.setHeight(block, value: 300) try engine.block.appendChild(to: page, child: block) let fill = try engine.block.createFill(.image) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(block, fill: fill) let slideAnimation = try engine.block.createAnimation(.slide) try engine.block.setInAnimation(block, animation: slideAnimation) try engine.block.setDuration(slideAnimation, duration: 1.0) let fadeOutAnimation = try engine.block.createAnimation(.fade) try engine.block.setOutAnimation(block, animation: fadeOutAnimation) let breathingLoop = try engine.block.createAnimation(.breathingLoop) try engine.block.setLoopAnimation(block, animation: breathingLoop) let inAnimation = try engine.block.getInAnimation(block) let outAnimation = try engine.block.getOutAnimation(block) let loopAnimation = try engine.block.getLoopAnimation(block) let inType = try engine.block.getType(inAnimation) let outType = try engine.block.getType(outAnimation) let currentDuration = try engine.block.getDuration(inAnimation) let currentEasing = try engine.block.getEnum(inAnimation, property: "animationEasing") let allProperties = try engine.block.findAllProperties(inAnimation) try engine.block.setDuration(inAnimation, duration: 0.8) try engine.block.setDuration(loopAnimation, duration: 2.0) try engine.block.setEnum(inAnimation, property: "animationEasing", value: "EaseOut") let easingOptions = try engine.block.getEnumValues(ofProperty: "animationEasing") try engine.block.setFloat( inAnimation, property: "animation/slide/direction", value: .pi, ) let direction = try engine.block.getFloat(inAnimation, property: "animation/slide/direction") let currentIn = try engine.block.getInAnimation(block) try engine.block.destroy(currentIn) let zoomAnimation = try engine.block.createAnimation(.zoom) try engine.block.setInAnimation(block, animation: zoomAnimation) try engine.block.setDuration(zoomAnimation, duration: 0.6) try engine.block.setEnum(zoomAnimation, property: "animationEasing", value: "EaseInOut") let currentLoop = try engine.block.getLoopAnimation(block) try engine.block.destroy(currentLoop) _ = (outType, inType, currentDuration, currentEasing, allProperties, easingOptions, direction, outAnimation) } ``` Modify existing animations by reading properties, changing duration and easing, and replacing or removing animations from blocks. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-edit-animations) Editing animations in CE.SDK involves retrieving existing animations from blocks and modifying their properties. This guide assumes you've already created and attached animations to blocks as covered in the [Base Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/base-0fc5c4/) guide. This guide covers retrieving animations, reading and modifying properties, changing easing functions, adjusting animation-specific settings, and replacing or removing animations. ## Retrieving Animations Before modifying an animation, retrieve it from the block using `getInAnimation`, `getOutAnimation`, or `getLoopAnimation`. Each returns an invalid `DesignBlockID` when no animation is attached to that slot, so guard with `if engine.block.isValid(animation)` before calling other APIs on the handle — `getType` and `getDuration` throw on an invalid handle. Once you have a valid handle, `getType` identifies the animation type (`//ly.img.ubq/animation/slide`, `//ly.img.ubq/animation/fade`, etc.). The example below skips the guard because all three slots were just populated in the same function; in production code, where a slot may be empty, always guard. ```swift highlight-editAnimations-retrieveAnimations let inAnimation = try engine.block.getInAnimation(block) let outAnimation = try engine.block.getOutAnimation(block) let loopAnimation = try engine.block.getLoopAnimation(block) let inType = try engine.block.getType(inAnimation) let outType = try engine.block.getType(outAnimation) ``` ## Reading Animation Properties Inspect current animation settings using property getters. `getDuration` returns the animation length in seconds, while `getEnum` retrieves values like easing functions. Use `findAllProperties` to discover all configurable properties for an animation. ```swift highlight-editAnimations-readProperties let currentDuration = try engine.block.getDuration(inAnimation) let currentEasing = try engine.block.getEnum(inAnimation, property: "animationEasing") let allProperties = try engine.block.findAllProperties(inAnimation) ``` Different animation types expose different properties — slide animations have direction, while loop animations may have intensity or scale properties. ## Modifying Animation Duration Change animation timing with `setDuration`. The duration is specified in seconds. ```swift highlight-editAnimations-modifyDuration try engine.block.setDuration(inAnimation, duration: 0.8) try engine.block.setDuration(loopAnimation, duration: 2.0) ``` When modifying In or Out animation durations, CE.SDK automatically adjusts the paired animation to prevent overlap. For loop animations, the duration defines the cycle length. ## Changing Easing Functions Easing controls animation acceleration. Use `setEnum` with the `"animationEasing"` property to change it. ```swift highlight-editAnimations-changeEasing try engine.block.setEnum(inAnimation, property: "animationEasing", value: "EaseOut") let easingOptions = try engine.block.getEnumValues(ofProperty: "animationEasing") ``` Use `getEnumValues(ofProperty: "animationEasing")` to discover available options: | Easing | Description | | --- | --- | | `Linear` | Constant speed throughout | | `EaseIn` | Starts slow, accelerates toward the end | | `EaseOut` | Starts fast, decelerates toward the end | | `EaseInOut` | Starts slow, speeds up, then slows down again | ## Adjusting Animation-Specific Properties Each animation type has unique configurable properties. For slide animations, change the entry direction using `setFloat` on `"animation/slide/direction"`. The value is the angle in radians that the block travels along during entrance — the block starts off-screen on the opposite side and slides in: - `0` — Slides right (enters from the left) - `0.5 * .pi` — Slides down (enters from the top) - `.pi` — Slides left (enters from the right) - `1.5 * .pi` — Slides up (enters from the bottom) ```swift highlight-editAnimations-adjustProperties try engine.block.setFloat( inAnimation, property: "animation/slide/direction", value: .pi, ) let direction = try engine.block.getFloat(inAnimation, property: "animation/slide/direction") ``` For text animations, adjust `"textAnimationWritingStyle"` (Block, Line, Word, Character) and `"textAnimationOverlap"` (0 for sequential, 1 for simultaneous) — see [Text Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/text-d6f4aa/) for details. ## Replacing Animations To swap an animation type, destroy the existing animation before setting a new one. This prevents memory leaks from orphaned animation objects. ```swift highlight-editAnimations-replaceAnimation let currentIn = try engine.block.getInAnimation(block) try engine.block.destroy(currentIn) let zoomAnimation = try engine.block.createAnimation(.zoom) try engine.block.setInAnimation(block, animation: zoomAnimation) try engine.block.setDuration(zoomAnimation, duration: 0.6) try engine.block.setEnum(zoomAnimation, property: "animationEasing", value: "EaseInOut") ``` ## Removing Animations Remove an animation by destroying it with `destroy`. After destruction, the getter returns an invalid `DesignBlockID` for that slot — `isValid(_:)` reports `false`. ```swift highlight-editAnimations-removeAnimation let currentLoop = try engine.block.getLoopAnimation(block) try engine.block.destroy(currentLoop) ``` Destroying a design block automatically destroys all its attached animations. Detached animations must be destroyed manually to free memory. ## API Reference | Method | Description | | --- | --- | | `engine.block.getInAnimation(block)` | Get entrance animation ID (invalid ID if none) | | `engine.block.getOutAnimation(block)` | Get exit animation ID (invalid ID if none) | | `engine.block.getLoopAnimation(block)` | Get loop animation ID (invalid ID if none) | | `engine.block.getType(anim)` | Get animation type string | | `engine.block.getDuration(anim)` | Get animation duration in seconds | | `engine.block.setDuration(anim, duration:)` | Set animation duration | | `engine.block.getEnum(anim, property:)` | Get enum property value | | `engine.block.setEnum(anim, property:, value:)` | Set enum property value | | `engine.block.getFloat(anim, property:)` | Get float property value | | `engine.block.setFloat(anim, property:, value:)` | Set float property value | | `engine.block.findAllProperties(anim)` | Get all available properties | | `engine.block.getEnumValues(ofProperty:)` | Get available values for an enum property | | `engine.block.destroy(anim)` | Destroy animation and free memory | ## Next Steps - [Supported Animation Types](https://img.ly/docs/cesdk/mac-catalyst/animation/types-4e5f41/) — Explore the animation types available in CE.SDK and their configurable properties - [Base Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/base-0fc5c4/) — Create entrance, exit, and loop animations - [Text Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/text-d6f4aa/) — Animate text with writing styles and character control - [Animation Overview](https://img.ly/docs/cesdk/mac-catalyst/animation/overview-6a2ef2/) — Understand animation concepts and capabilities --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Keyframes" description: "Animate single block properties over time with keyframes, easing curves and absolute or relative tracks." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/) --- ```swift file=@cesdk_swift_examples/engine-guides-keyframes/Keyframes.swift reference-only import Foundation import IMGLYEngine @MainActor func keyframes(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) // A 5 second rectangle with a color fill and a rect shape let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.setWidth(block, value: 600) try engine.block.setHeight(block, value: 400) try engine.block.setPositionX(block, value: 360) try engine.block.setPositionY(block, value: 340) try engine.block.appendChild(to: page, child: block) try engine.block.setDuration(block, duration: 5) // The fill and the shape own their properties and their keyframes let fill = try engine.block.getFill(block) let shape = try engine.block.getShape(block) let opacityIsAnimatable = try engine.block.isPropertyAnimatable(block, property: "opacity") let fillColorIsAnimatable = try engine.block.isPropertyAnimatable(fill, property: "fill/color/value") let visibleIsAnimatable = try engine.block.isPropertyAnimatable(block, property: "visible") print("opacity: \(opacityIsAnimatable), fill color: \(fillColorIsAnimatable), visible: \(visibleIsAnimatable)") // Fade in during the first 40% of the block's duration (2 seconds) try engine.block.setKeyframes(block, property: "opacity", keyframes: [ Keyframe(time: 0, value: .float(0), easing: .preset(.easeOut)), Keyframe(time: 0.4, value: .float(1)), ]) // Orange, then a hold on purple, then a fade to teal try engine.block.setKeyframes(fill, property: "fill/color/value", keyframes: [ Keyframe(time: 0, value: .color(.rgba(r: 1, g: 0.5, b: 0, a: 1))), Keyframe(time: 0.5, value: .color(.rgba(r: 0.5, g: 0.2, b: 0.9, a: 1)), easing: .hold), Keyframe(time: 0.75, value: .color(.rgba(r: 0.4, g: 0.8, b: 0.8, a: 1))), Keyframe(time: 1, value: .color(.rgba(r: 0, g: 0.6, b: 0.6, a: 1))), ]) // Grow every corner radius with a CSS-style cubic bezier for corner in ["TL", "TR", "BL", "BR"] { try engine.block.setKeyframes(shape, property: "shape/rect/cornerRadius\(corner)", keyframes: [ Keyframe(time: 0, value: .float(0), easing: .bezier(out: SIMD2(0.7, 0), in: SIMD2(0.3, 1))), Keyframe(time: 1, value: .float(200)), ]) } // Slide 600 units to the right of the block's static x position try engine.block.setKeyframes(block, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(0)), Keyframe(time: 1, value: .float(600)), ], mode: .relative) // Switch the stroke on halfway; discrete values never interpolate try engine.block.setStrokeWidth(block, width: 12) try engine.block.setStrokeColor(block, color: .rgba(r: 0, g: 0, b: 0, a: 1)) try engine.block.setKeyframes(block, property: "stroke/enabled", keyframes: [ Keyframe(time: 0, value: .discrete(.bool(false))), Keyframe(time: 0.5, value: .discrete(.bool(true))), ]) // A dot follows a curved path. Vec2 keyframes animate `position/x` and // `position/y` together, and the tangents bend the path upward. let dot = try engine.block.create(.graphic) try engine.block.setShape(dot, shape: engine.block.createShape(.ellipse)) let dotFill = try engine.block.createFill(.color) try engine.block.setColor(dotFill, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.2, b: 0.4, a: 1)) try engine.block.setFill(dot, fill: dotFill) try engine.block.setWidth(dot, value: 120) try engine.block.setHeight(dot, value: 120) try engine.block.appendChild(to: page, child: dot) try engine.block.setDuration(dot, duration: 5) // The static position is the start of the path. try engine.block.setVec2(dot, property: "position", value: [200, 900]) try engine.block.setKeyframes(dot, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([200, 900]), spatialOut: [300, -600], spatialIn: [-300, -600]), Keyframe(time: 1, value: .vec2([1600, 900])), ]) // Pulse the dot to 1.5 times its size and back. Its frame does not change. for axis in ["x", "y"] { try engine.block.setKeyframes(dot, property: "transform/scale/\(axis)", keyframes: [ Keyframe(time: 0, value: .float(1)), Keyframe(time: 0.5, value: .float(1.5)), Keyframe(time: 1, value: .float(1)), ]) } print(try engine.block.getAnimatedProperties(dot)) // ["position", "transform/scale/x", "transform/scale/y"] // ["opacity", "position/x", "stroke/enabled"] let animatedProperties = try engine.block.getAnimatedProperties(block) // The second keyframe reports the default easing, .preset(.linear) let opacityKeyframes = try engine.block.getKeyframes(block, property: "opacity") // .relative let positionMode = try engine.block.getKeyframeMode(block, property: "position/x") print(animatedProperties, opacityKeyframes[1].easing, positionMode == .relative) // Changes the static value only. The absolute opacity track still replaces it. try engine.block.setOpacity(block, value: 0.5) let opacityKeyframeCount = try engine.block.getKeyframes(block, property: "opacity").count print(opacityKeyframeCount) // 2 // The bottom corners return to their static radius of 0 try engine.block.removeKeyframes(shape, property: "shape/rect/cornerRadiusBL") try engine.block.removeKeyframes(shape, property: "shape/rect/cornerRadiusBR") let shapeAnimatedProperties = try engine.block.getAnimatedProperties(shape) print(shapeAnimatedProperties) // ["shape/rect/cornerRadiusTL", "shape/rect/cornerRadiusTR"] do { try engine.block.setKeyframes(block, property: "visible", keyframes: [ Keyframe(time: 0, value: .discrete(.bool(true))), ]) } catch let error as EngineError { print(error.catalogCode == .blockKeyframesPropertyNotAnimatable) // true } // Export the frame at 2.5 seconds, halfway through the animation try engine.block.setPlaybackTime(page, time: 2.5) let png = try await engine.block.export(page, mimeType: .png) print("Exported \(png.count) bytes") try await engine.captureGuide(page, label: "hero") // A bar that leans by 20 degrees and turns half a turn about its centre. // The anchor is in design units from the top-left corner of the frame. let bar = try engine.block.create(.graphic) try engine.block.setShape(bar, shape: engine.block.createShape(.rect)) try engine.block.setFill(bar, fill: engine.block.createFill(.color)) try engine.block.setWidth(bar, value: 320) try engine.block.setHeight(bar, value: 80) try engine.block.setPositionX(bar, value: 120) try engine.block.setPositionY(bar, value: 120) try engine.block.appendChild(to: page, child: bar) try engine.block.setDuration(bar, duration: 5) try engine.block.setFloat(bar, property: "transform/anchor/x", value: 160) try engine.block.setFloat(bar, property: "transform/anchor/y", value: 40) try engine.block.setFloat(bar, property: "transform/skew", value: .pi / 9) try engine.block.setKeyframes(bar, property: "rotation", keyframes: [ Keyframe(time: 0, value: .float(0)), Keyframe(time: 1, value: .float(.pi)), ]) // The bar has an anchor, so its rotation track turns it about the anchor. // A graphic or text without an anchor turns about its frame centre. print(try engine.block.hasTransformAnchor(bar)) // true // A bar on a wavy path turns to follow the direction of its motion let arrow = try engine.block.create(.graphic) try engine.block.setShape(arrow, shape: engine.block.createShape(.rect)) let arrowFill = try engine.block.createFill(.color) try engine.block.setColor(arrowFill, property: "fill/color/value", color: .rgba(r: 0.1, g: 0.6, b: 0.3, a: 1)) try engine.block.setFill(arrow, fill: arrowFill) try engine.block.setWidth(arrow, value: 160) try engine.block.setHeight(arrow, value: 40) try engine.block.appendChild(to: page, child: arrow) try engine.block.setDuration(arrow, duration: 5) try engine.block.setFloat(arrow, property: "transform/anchor/x", value: 80) try engine.block.setFloat(arrow, property: "transform/anchor/y", value: 20) try engine.block.setVec2(arrow, property: "position", value: [700, 140]) try engine.block.setKeyframes(arrow, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([700, 140]), spatialOut: [300, 150], spatialIn: [-300, -150]), Keyframe(time: 1, value: .vec2([1600, 140])), ]) try engine.block.setBool(arrow, property: "transform/autoOrient", value: true) // A square rides in front of the arrow. Its position is read in the // arrow's local space, so it moves and turns with the arrow. let rider = try engine.block.create(.graphic) try engine.block.setShape(rider, shape: engine.block.createShape(.rect)) let riderFill = try engine.block.createFill(.color) try engine.block.setColor(riderFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.7, b: 0.1, a: 1)) try engine.block.setFill(rider, fill: riderFill) try engine.block.setWidth(rider, value: 40) try engine.block.setHeight(rider, value: 40) try engine.block.setPositionX(rider, value: 170) try engine.block.setPositionY(rider, value: 0) try engine.block.appendChild(to: page, child: rider) try engine.block.setTransformParent(rider, parent: arrow) print(try engine.block.getTransformParent(rider) == arrow) // true do { // The arrow cannot follow a block that follows the arrow try engine.block.setTransformParent(arrow, parent: rider) } catch let error as EngineError { print(error.catalogCode == .blockTransformParentCycle) // true } } ``` Animate single block properties over time with keyframes, such as an opacity fade, a color change or a growing corner radius. ![A purple block with a stroke and rounded top corners, exported at 2.5 seconds](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-keyframes) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Preset in, out and loop animations cover common motion. Keyframes give you control over each value, each time and each curve. A keyframe track belongs to one property of one block and renders at the current playback time, in frame exports and in video export. For preset animations, see [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/). This guide covers how to check which properties accept keyframes, how time and values work, easing, absolute and relative tracks, anchor, skew and auto-orient, how a block follows another block, reading tracks back, removing them, and the errors to expect. ## Creating the Animated Block We create a rectangle with a color fill and a rect shape, and give it a duration of 5 seconds. Keyframes belong to the block that owns the property. The graphic owns `opacity` and `position/x`, the fill owns `fill/color/value` and the shape owns the corner radii. ```swift highlight-keyframes-createBlock // A 5 second rectangle with a color fill and a rect shape let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.setWidth(block, value: 600) try engine.block.setHeight(block, value: 400) try engine.block.setPositionX(block, value: 360) try engine.block.setPositionY(block, value: 340) try engine.block.appendChild(to: page, child: block) try engine.block.setDuration(block, duration: 5) // The fill and the shape own their properties and their keyframes let fill = try engine.block.getFill(block) let shape = try engine.block.getShape(block) ``` ## Checking Animatable Properties `isPropertyAnimatable(_:property:)` tells you whether a property of a block accepts keyframes. It returns `false` for a property that the block does not have, for example a star property on a rect shape, and for a property that cannot be animated, such as `visible`. ```swift highlight-keyframes-checkAnimatable let opacityIsAnimatable = try engine.block.isPropertyAnimatable(block, property: "opacity") let fillColorIsAnimatable = try engine.block.isPropertyAnimatable(fill, property: "fill/color/value") let visibleIsAnimatable = try engine.block.isPropertyAnimatable(block, property: "visible") print("opacity: \(opacityIsAnimatable), fill color: \(fillColorIsAnimatable), visible: \(visibleIsAnimatable)") ``` Pass the block that owns the property. `fill/color/value` is animatable on the fill block, not on the graphic. ## Setting Keyframes `setKeyframes(_:property:keyframes:mode:)` replaces the whole track of a property. Each `Keyframe` has a `time` from 0 to 1, normalized over the block's duration. A fill, shape or effect uses the duration of the block it belongs to. Here the fade ends at `0.4`, which is 2 seconds into the 5 second block. ```swift highlight-keyframes-fadeIn // Fade in during the first 40% of the block's duration (2 seconds) try engine.block.setKeyframes(block, property: "opacity", keyframes: [ Keyframe(time: 0, value: .float(0), easing: .preset(.easeOut)), Keyframe(time: 0.4, value: .float(1)), ]) ``` The engine sorts the keyframes by time. Before the first keyframe the property keeps the first value, and after the last keyframe it keeps the last value. When you change the block's duration, the animation stretches with it. ## Keyframe Values The `KeyframeValue` case must match the kind of the property, and all keyframes of one track use the same kind. | Value | Holds | Example property | | --- | --- | --- | | `.float` | A `Float` | `opacity`, `rotation`, `shape/rect/cornerRadiusTL` | | `.color` | A `Color`: RGBA, CMYK or spot | `fill/color/value` | | `.vec2` | A `SIMD2` | `position`, `shape/position` | | `.list` | An array of `Float` | `stroke/dashArray`, `fill/gradient/colors` | | `.path` | `PathVertex` items and a `closed` flag | `shape/vector_path/path` | | `.discrete` | A `DiscreteValue`: `.bool`, `.int` or `.string` | `stroke/enabled`, `blend/mode`, `text/text` | All color keyframes of a track must use one color space. ```swift highlight-keyframes-colorHold // Orange, then a hold on purple, then a fade to teal try engine.block.setKeyframes(fill, property: "fill/color/value", keyframes: [ Keyframe(time: 0, value: .color(.rgba(r: 1, g: 0.5, b: 0, a: 1))), Keyframe(time: 0.5, value: .color(.rgba(r: 0.5, g: 0.2, b: 0.9, a: 1)), easing: .hold), Keyframe(time: 0.75, value: .color(.rgba(r: 0.4, g: 0.8, b: 0.8, a: 1))), Keyframe(time: 1, value: .color(.rgba(r: 0, g: 0.6, b: 0.6, a: 1))), ]) ``` A `.discrete` value never interpolates. It changes at the time of the next keyframe. An `.int` value selects an enum option by its index, and an index that names no option throws `.blockEnumValueInvalid`. ```swift highlight-keyframes-discrete // Switch the stroke on halfway; discrete values never interpolate try engine.block.setStrokeWidth(block, width: 12) try engine.block.setStrokeColor(block, color: .rgba(r: 0, g: 0, b: 0, a: 1)) try engine.block.setKeyframes(block, property: "stroke/enabled", keyframes: [ Keyframe(time: 0, value: .discrete(.bool(false))), Keyframe(time: 0.5, value: .discrete(.bool(true))), ]) ``` ## Easing Between Keyframes The `easing` of a keyframe shapes the curve from that keyframe to the next one. It defaults to `.preset(.linear)`. - `.preset(_:)` uses one of the `AnimationEasing` presets, the same values as the `animationEasing` property of preset animations. - `.bezier(out:in:)` is a cubic bezier, as in CSS `cubic-bezier(out.x, out.y, in.x, in.y)`. Both x values must be from 0 to 1. - `.hold` keeps the value until the next keyframe. In the color track above, the purple keyframe holds from 50% to 75% and then jumps. We grow all four corner radii with a bezier curve that starts slow and ends slow. ```swift highlight-keyframes-bezier // Grow every corner radius with a CSS-style cubic bezier for corner in ["TL", "TR", "BL", "BR"] { try engine.block.setKeyframes(shape, property: "shape/rect/cornerRadius\(corner)", keyframes: [ Keyframe(time: 0, value: .float(0), easing: .bezier(out: SIMD2(0.7, 0), in: SIMD2(0.3, 1))), Keyframe(time: 1, value: .float(200)), ]) } ``` ## Absolute and Relative Tracks A track is `.absolute` by default, so its value replaces the static value of the property. Pass `mode: .relative` to add the track value to the static value. Relative tracks work for number and vec2 properties only. ```swift highlight-keyframes-relative // Slide 600 units to the right of the block's static x position try engine.block.setKeyframes(block, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(0)), Keyframe(time: 1, value: .float(600)), ], mode: .relative) ``` This relative `position/x` track moves the block 600 units to the right of wherever it is placed. When you move the block, the whole motion moves with it. ## Motion Paths and Scale `position` of a block is a vec2 property, so one track moves both axes. The dot follows a curved path: its `spatialOut` and `spatialIn` tangents bend the path between two keyframes. ```swift highlight-keyframes-motionPath // A dot follows a curved path. Vec2 keyframes animate `position/x` and // `position/y` together, and the tangents bend the path upward. let dot = try engine.block.create(.graphic) try engine.block.setShape(dot, shape: engine.block.createShape(.ellipse)) let dotFill = try engine.block.createFill(.color) try engine.block.setColor(dotFill, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.2, b: 0.4, a: 1)) try engine.block.setFill(dot, fill: dotFill) try engine.block.setWidth(dot, value: 120) try engine.block.setHeight(dot, value: 120) try engine.block.appendChild(to: page, child: dot) try engine.block.setDuration(dot, duration: 5) // The static position is the start of the path. try engine.block.setVec2(dot, property: "position", value: [200, 900]) try engine.block.setKeyframes(dot, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([200, 900]), spatialOut: [300, -600], spatialIn: [-300, -600]), Keyframe(time: 1, value: .vec2([1600, 900])), ]) ``` For which keyframe holds which tangent, straight paths, single-axis tracks and the errors to expect, see [Motion Paths](https://img.ly/docs/cesdk/mac-catalyst/animation/motion-paths-7c619f/). Rotation and scale tracks pivot on the block's anchor, `transform/anchor/x` and `transform/anchor/y`, in design units from the top-left corner of the frame. A block without an anchor turns about its center, as `setRotation(_:radians:)` turns it, and scales about its top-left corner. Set the anchor, even to (0, 0), to make it the pivot of the rotation too. `hasTransformAnchor(_:)` tells whether a block has an anchor. `transform/scale/x` and `transform/scale/y` scale a graphic, text or caption block about its anchor without changing its frame. A negative value mirrors the block, so a track that shows another sign than the static scale also needs the `layer/flip` scope. `transform/scale` takes `'Vec2'` keyframes and writes both axis tracks at once. It reads them back as one track while the two axes share their times and easings. ```swift highlight-keyframes-scale // Pulse the dot to 1.5 times its size and back. Its frame does not change. for axis in ["x", "y"] { try engine.block.setKeyframes(dot, property: "transform/scale/\(axis)", keyframes: [ Keyframe(time: 0, value: .float(1)), Keyframe(time: 0.5, value: .float(1.5)), Keyframe(time: 1, value: .float(1)), ]) } print(try engine.block.getAnimatedProperties(dot)) // ["position", "transform/scale/x", "transform/scale/y"] ``` Scale tracks take `.float` keyframes. `playback/timeRemap` of a composition group also takes them; see [Time Remap](https://img.ly/docs/cesdk/mac-catalyst/animation/time-remap-063ab0/). ## Anchor and Skew `transform/anchor/x` and `transform/anchor/y` set the point that rotation, scale and skew pivot on, and `transform/skew` shears the block by an angle in radians, like `rotation`. The bar below has its anchor at its center and leans by 20 degrees (`.pi / 9`), so its rotation track turns it in place. ```swift highlight-keyframes-anchorSkew // A bar that leans by 20 degrees and turns half a turn about its centre. // The anchor is in design units from the top-left corner of the frame. let bar = try engine.block.create(.graphic) try engine.block.setShape(bar, shape: engine.block.createShape(.rect)) try engine.block.setFill(bar, fill: engine.block.createFill(.color)) try engine.block.setWidth(bar, value: 320) try engine.block.setHeight(bar, value: 80) try engine.block.setPositionX(bar, value: 120) try engine.block.setPositionY(bar, value: 120) try engine.block.appendChild(to: page, child: bar) try engine.block.setDuration(bar, duration: 5) try engine.block.setFloat(bar, property: "transform/anchor/x", value: 160) try engine.block.setFloat(bar, property: "transform/anchor/y", value: 40) try engine.block.setFloat(bar, property: "transform/skew", value: .pi / 9) try engine.block.setKeyframes(bar, property: "rotation", keyframes: [ Keyframe(time: 0, value: .float(0)), Keyframe(time: 1, value: .float(.pi)), ]) ``` `hasTransformAnchor(_:)` tells whether a block has an anchor. Setting `transform/anchor/x` or `transform/anchor/y`, even to 0, or giving one of them keyframes, gives a block an anchor. A rotation track turns a graphic or text with an anchor about the anchor, and one without about its frame center. ```swift highlight-keyframes-hasAnchor // The bar has an anchor, so its rotation track turns it about the anchor. // A graphic or text without an anchor turns about its frame centre. print(try engine.block.hasTransformAnchor(bar)) // true ``` ## Auto-Orient When `transform/autoOrient` is on, a block turns to follow the direction of its position keyframes. ```swift highlight-keyframes-autoOrient // A bar on a wavy path turns to follow the direction of its motion let arrow = try engine.block.create(.graphic) try engine.block.setShape(arrow, shape: engine.block.createShape(.rect)) let arrowFill = try engine.block.createFill(.color) try engine.block.setColor(arrowFill, property: "fill/color/value", color: .rgba(r: 0.1, g: 0.6, b: 0.3, a: 1)) try engine.block.setFill(arrow, fill: arrowFill) try engine.block.setWidth(arrow, value: 160) try engine.block.setHeight(arrow, value: 40) try engine.block.appendChild(to: page, child: arrow) try engine.block.setDuration(arrow, duration: 5) try engine.block.setFloat(arrow, property: "transform/anchor/x", value: 80) try engine.block.setFloat(arrow, property: "transform/anchor/y", value: 20) try engine.block.setVec2(arrow, property: "position", value: [700, 140]) try engine.block.setKeyframes(arrow, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([700, 140]), spatialOut: [300, 150], spatialIn: [-300, -150]), Keyframe(time: 1, value: .vec2([1600, 140])), ]) try engine.block.setBool(arrow, property: "transform/autoOrient", value: true) ``` The arrow turns about its anchor at its center. For a block without an anchor, the skew axis, animating the skew, how auto-orient adds to the rotation and the errors to expect, see [Anchor, Skew and Auto-Orient](https://img.ly/docs/cesdk/mac-catalyst/animation/anchor-skew-auto-orient-ede5e9/). ## Follow Another Block `setTransformParent(_:parent:)` makes a block follow the transform of another block, so it moves with the block also while that block plays keyframes. In the example, the square rides along the arrow's path. `getTransformParent(_:)` returns the transform parent, or `nil`. ```swift highlight-keyframes-transformParent // A square rides in front of the arrow. Its position is read in the // arrow's local space, so it moves and turns with the arrow. let rider = try engine.block.create(.graphic) try engine.block.setShape(rider, shape: engine.block.createShape(.rect)) let riderFill = try engine.block.createFill(.color) try engine.block.setColor(riderFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.7, b: 0.1, a: 1)) try engine.block.setFill(rider, fill: riderFill) try engine.block.setWidth(rider, value: 40) try engine.block.setHeight(rider, value: 40) try engine.block.setPositionX(rider, value: 170) try engine.block.setPositionY(rider, value: 0) try engine.block.appendChild(to: page, child: rider) try engine.block.setTransformParent(rider, parent: arrow) print(try engine.block.getTransformParent(rider) == arrow) // true do { // The arrow cannot follow a block that follows the arrow try engine.block.setTransformParent(arrow, parent: rider) } catch let error as EngineError { print(error.catalogCode == .blockTransformParentCycle) // true } ``` For what a block inherits from its transform parent, chains of links, removing a link and the errors to expect, see [Follow Another Block](https://img.ly/docs/cesdk/mac-catalyst/animation/transform-parent-b51eab/). ## Reading Keyframes Back `getAnimatedProperties(_:)` lists the paths of a block that have keyframes, sorted. `getKeyframes(_:property:)` returns the keyframes of a property sorted by time, with the default easing filled in. It returns an empty array for an animatable property without keyframes. `getKeyframeMode(_:property:)` returns the `TrackMode` of a track. ```swift highlight-keyframes-readBack // ["opacity", "position/x", "stroke/enabled"] let animatedProperties = try engine.block.getAnimatedProperties(block) // The second keyframe reports the default easing, .preset(.linear) let opacityKeyframes = try engine.block.getKeyframes(block, property: "opacity") // .relative let positionMode = try engine.block.getKeyframeMode(block, property: "position/x") print(animatedProperties, opacityKeyframes[1].easing, positionMode == .relative) ``` `getKeyframeMode` throws for a property without keyframes, so check `getAnimatedProperties` first when you do not know. ## Static Values and Keyframes Setters such as `setOpacity(_:value:)` or `setFloat(_:property:value:)` change the static value and leave the keyframes as they are. `setKeyframes` does not change the static value either. ```swift highlight-keyframes-staticValue // Changes the static value only. The absolute opacity track still replaces it. try engine.block.setOpacity(block, value: 0.5) let opacityKeyframeCount = try engine.block.getKeyframes(block, property: "opacity").count print(opacityKeyframeCount) // 2 ``` An absolute track replaces the static value, so the new opacity does not show while the track exists. A relative track adds to it. ## Removing Keyframes `removeKeyframes(_:property:)` removes the track of one property, so the static value applies again. Removing from a property without keyframes does nothing. To change single keyframes, read the track with `getKeyframes`, edit the array and pass it to `setKeyframes`. ```swift highlight-keyframes-remove // The bottom corners return to their static radius of 0 try engine.block.removeKeyframes(shape, property: "shape/rect/cornerRadiusBL") try engine.block.removeKeyframes(shape, property: "shape/rect/cornerRadiusBR") let shapeAnimatedProperties = try engine.block.getAnimatedProperties(shape) print(shapeAnimatedProperties) // ["shape/rect/cornerRadiusTL", "shape/rect/cornerRadiusTR"] ``` ## Handling Errors Invalid input throws an `EngineError`. Its `catalogCode` names the problem as an `EngineErrorCode`. A failed `setKeyframes` leaves the existing track unchanged. ```swift highlight-keyframes-errors do { try engine.block.setKeyframes(block, property: "visible", keyframes: [ Keyframe(time: 0, value: .discrete(.bool(true))), ]) } catch let error as EngineError { print(error.catalogCode == .blockKeyframesPropertyNotAnimatable) // true } ``` ## Exporting a Frame We set the page's playback time to 2.5 seconds and export the page as PNG. The export shows every track at that time: the block is halfway along its path, purple, with a stroke and rounded top corners. ```swift highlight-keyframes-export // Export the frame at 2.5 seconds, halfway through the animation try engine.block.setPlaybackTime(page, time: 2.5) let png = try await engine.block.export(page, mimeType: .png) print("Exported \(png.count) bytes") ``` ## Keyframes and Other Block Operations Keyframes take part in the usual block operations: - **Undo and redo** restore keyframe tracks. - **Saving and loading** a scene keeps the tracks, their easing and their mode. - **Duplicating** a block copies the tracks of the block, its fill and its shape. - **Replacing** a fill, shape or effect does not move the keyframes to the new block. They stay with the old one. - **Preset animations** run on top of keyframe tracks. A fade animation scales the keyframed opacity. A scale animation (Zoom, Pop, Grow, Scale Loop, Squeeze) ignores the static or keyframed `transform/scale` of the block while it runs. Pulsating and Breathing scale the static or keyframed scale, so a flipped block stays flipped. - **Scaling** a block or **changing the design unit** also converts the keyframes of length and position properties, such as corner radii and `position/x`. - **Rotating** or **flipping** a block turns or mirrors the keyframes of its position, rotation and skew with it. The keyframes of a group turn and mirror with the group. A turn other than a half turn joins `position/x` and `position/y` tracks into one `position` track when they share the mode, the times and the easings and no `position` track exists. Other axis tracks cannot turn: their Absolute values move with the block, and their Relative values stay. `setKeyframes` and `removeKeyframes` need the `appearance/animation` scope and the scope of the property's setter, such as `layer/move` for `position`, `layer/rotate` for `rotation` and `layer/resize` for `width` and the scale. A `transform/scale/x` or `transform/scale/y` track that shows another sign than the static scale also needs `layer/flip`. A transform lock (`setTransformLocked`) does not apply to keyframes: tracks on the position, rotation, size, scale, anchor or skew still animate a locked block. ## Troubleshooting - **The property does not accept keyframes** (`.blockKeyframesPropertyNotAnimatable`): check `isPropertyAnimatable` and pass the block that owns the property, for example the fill for `fill/color/value`. All keyframe methods throw this error for such a property. - **The keyframe list is empty** (`.blockKeyframesEmpty`): pass at least one keyframe. Use `removeKeyframes` to remove a track. - **A time is outside 0 to 1** (`.blockKeyframesTimeOutOfRange`): times are fractions of the block's duration, not seconds. - **Two keyframes have the same time** (`.blockKeyframesDuplicateTime`): give each keyframe its own time. - **The value does not fit the property** (`.blockKeyframesValueKindMismatch`): use the value type of the property, for example `.color` for `fill/color/value`. - **The keyframes hold different value types** (`.blockKeyframesMixedValueKinds`): use one value type for the whole track. - **The colors use different color spaces** (`.blockKeyframesMixedColorSpaces`): use one color space per track. For a spot color, keep one spot color name and animate its tint. - **A bezier handle is out of range** (`.blockKeyframesEasingOutOfRange`): keep the x values of `out` and `in` from 0 to 1. - **The relative mode fails** (`.blockKeyframesRelativeUnsupported`): use `.relative` only for number and vec2 properties. - **Motion path tangents fail** (`.blockKeyframesSpatialTangentUnsupported`): `spatialOut` and `spatialIn` work only on keyframes with a `.vec2` value. - **Reading the mode fails** (`.blockKeyframesNoTrack`): the property has no keyframes. - **A block cannot follow another block** (`.blockTransformParentCycle`): the parent is the block itself, sits inside it or follows it, directly or through other blocks. Pick a parent outside that chain. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.isPropertyAnimatable(_:property:)` | Check whether a property of a block accepts keyframes | | `engine.block.setKeyframes(_:property:keyframes:mode:)` | Replace the keyframe track of a property | | `engine.block.getKeyframes(_:property:)` | Get the keyframes of a property, sorted by time | | `engine.block.getKeyframeMode(_:property:)` | Get the `TrackMode` of a track | | `engine.block.getAnimatedProperties(_:)` | List the properties of a block that have keyframes | | `engine.block.removeKeyframes(_:property:)` | Remove the keyframe track of a property | | `engine.block.setVec2(_:property:value:)` | Set the static value of `position` or `shape/position` | | `engine.block.setFloat(_:property:value:)` | Set `transform/anchor/x`, `transform/anchor/y` or `transform/skew` | | `engine.block.setBool(_:property:value:)` | Turn `transform/autoOrient` on | | `engine.block.setTransformParent(_:parent:)` | Make a block follow the transform of another block, or pass `nil` to remove the link | | `engine.block.getTransformParent(_:)` | Get the block that a block follows, or `nil` | | `engine.block.hasTransformAnchor(_:)` | Check whether a block has an anchor, the pivot of a `rotation` track | | `engine.block.getFill(_:)` | Get the fill block that owns the fill color | | `engine.block.getShape(_:)` | Get the shape block that owns the corner radii | | `engine.block.setDuration(_:duration:)` | Set the duration that keyframe times refer to | | `engine.block.setOpacity(_:value:)` | Set the static opacity | | `engine.block.setPlaybackTime(_:time:)` | Set the time to render | | `engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:)` | Export the page at the current playback time | ## Next Steps - [Follow Another Block](https://img.ly/docs/cesdk/mac-catalyst/animation/transform-parent-b51eab/): make blocks follow the transform of another block. - [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/): add preset in, out and loop animations to blocks. - [Edit Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/edit-32c12a/): change the duration, easing and properties of preset animations. - [Animation Overview](https://img.ly/docs/cesdk/mac-catalyst/animation/overview-6a2ef2/): understand animation concepts and capabilities. - [Anchor, Skew and Auto-Orient](https://img.ly/docs/cesdk/mac-catalyst/animation/anchor-skew-auto-orient-ede5e9/): set the pivot of a block, skew it and turn it along its motion path. - [Motion Paths](https://img.ly/docs/cesdk/mac-catalyst/animation/motion-paths-7c619f/): move blocks along straight and curved paths. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Motion Blur" description: "Blur moving blocks in exported videos and in the preview with a shutter angle, a shutter phase and a number of samples." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/motion-blur-122f43/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Motion Blur](https://img.ly/docs/cesdk/mac-catalyst/animation/motion-blur-122f43/) --- ```swift file=@cesdk_swift_examples/engine-guides-motion-blur/MotionBlur.swift reference-only import Foundation import IMGLYEngine @MainActor func motionBlur(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) try engine.block.setDuration(page, duration: 2) // A square that crosses the page in one second. Motion blur needs motion. let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.2, b: 0.4, a: 1)) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: 300) try engine.block.setHeight(block, value: 300) try engine.block.setPositionY(block, value: 390) try engine.block.appendChild(to: page, child: block) try engine.block.setDuration(block, duration: 2) try engine.block.setKeyframes(block, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(100)), Keyframe(time: 0.5, value: .float(1520)), Keyframe(time: 1, value: .float(100)), ]) // Blur the block while it moves try engine.block.setBool(block, property: "motionBlur/enabled", value: true) print(try engine.block.getBool(block, property: "motionBlur/enabled")) // true // Graphic, text and caption blocks have the property. Pages do not. let blockHasMotionBlur = try engine.block.findAllProperties(block).contains("motionBlur/enabled") let pageHasMotionBlur = try engine.block.findAllProperties(page).contains("motionBlur/enabled") print(blockHasMotionBlur, pageHasMotionBlur) // true false // The scene holds the shutter for all blocks with motion blur. // 360 degrees keeps the shutter open for one full frame. try engine.block.setFloat(scene, property: "motionBlur/shutterAngle", value: 360) // -180 degrees, half the angle, centers the shutter on the frame time try engine.block.setFloat(scene, property: "motionBlur/shutterPhase", value: -180) // More samples give a smoother trail and take longer to render try engine.block.setInt(scene, property: "motionBlur/samples", value: 32) print(try engine.block.getInt(scene, property: "motionBlur/samples")) // 32 // One frame of the scene frame rate is the span the shutter angle refers to try engine.block.setFloat(scene, property: "scene/frameRate", value: 30) // The preview draws the motion blur by default. // Set the value to false to show the blocks sharp while editing. try engine.editor.setSettingBool("features/previewMotionBlurEnabled", value: false) print(try engine.editor.getSettingBool("features/previewMotionBlurEnabled")) // false do { try engine.block.setInt(scene, property: "motionBlur/samples", value: 100) } catch let error as EngineError { print(error.catalogCode == .blockMotionBlurValueOutOfRange) // true } // Image export draws no motion blur. Export a video to see it in a file. let stream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in stream { if case let .finished(video: video) = event { try video.write(to: FileManager.default.temporaryDirectory.appendingPathComponent("motion-blur.mp4")) } } } ``` Blur moving blocks in exported videos and in the preview, as a camera shutter does. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-motion-blur) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Motion blur smears a moving block along its path during one frame. A block with motion blur draws the average of several samples over the time that the shutter stays open. The blur renders in video export and in the preview. The editor has no motion blur control, so you set it with the block API. This guide covers how to turn motion blur on for a block, how to tune the shutter and the samples of the scene, how the frame rate sets the length of the blur, the preview blur and its setting, and video export. ## Animating the Block Motion blur needs motion. A block blurs only while something moves it during the shutter: keyframes, a preset animation, the motion of its group, or a Lottie animation in its video fill. A block that stands still draws unchanged. We move a square across the page in 1 second with `position/x` keyframes, and back. For keyframes, see [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/). ```swift highlight-motionBlur-animateBlock // A square that crosses the page in one second. Motion blur needs motion. let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.2, b: 0.4, a: 1)) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: 300) try engine.block.setHeight(block, value: 300) try engine.block.setPositionY(block, value: 390) try engine.block.appendChild(to: page, child: block) try engine.block.setDuration(block, duration: 2) try engine.block.setKeyframes(block, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(100)), Keyframe(time: 0.5, value: .float(1520)), Keyframe(time: 1, value: .float(100)), ]) ``` ## Turning Motion Blur On The block property `motionBlur/enabled` turns motion blur on for one block. It is `false` by default. The setter needs the `appearance/animation` scope. ```swift highlight-motionBlur-enable // Blur the block while it moves try engine.block.setBool(block, property: "motionBlur/enabled", value: true) print(try engine.block.getBool(block, property: "motionBlur/enabled")) // true ``` Graphic, text and caption blocks have the property. Pages, scenes, groups and fills do not, and setting it on them throws. Use `findAllProperties(_:)` to check a block. ```swift highlight-motionBlur-checkSupport // Graphic, text and caption blocks have the property. Pages do not. let blockHasMotionBlur = try engine.block.findAllProperties(block).contains("motionBlur/enabled") let pageHasMotionBlur = try engine.block.findAllProperties(page).contains("motionBlur/enabled") print(blockHasMotionBlur, pageHasMotionBlur) // true false ``` A group does not blur as a whole. Turn motion blur on for its children, and they blur when the group moves. ## Shutter Angle and Phase The scene holds one shutter for all blocks with motion blur. Both values are in degrees of one frame. - `motionBlur/shutterAngle` sets how long the shutter stays open. The default is 180, which is half a frame. The range is 0 to 720. A larger angle draws a longer trail, and 0 draws no blur. - `motionBlur/shutterPhase` sets where the shutter opens, relative to the frame time. The default is 0, so the trail lies ahead of the block. The range is -360 to 360. A phase of minus half the angle centers the blur on the frame time. A phase of minus the angle puts the trail behind the block. ```swift highlight-motionBlur-shutter // The scene holds the shutter for all blocks with motion blur. // 360 degrees keeps the shutter open for one full frame. try engine.block.setFloat(scene, property: "motionBlur/shutterAngle", value: 360) // -180 degrees, half the angle, centers the shutter on the frame time try engine.block.setFloat(scene, property: "motionBlur/shutterPhase", value: -180) ``` ## Samples `motionBlur/samples` sets how many samples one frame averages. The default is 16 and the range is 1 to 64. More samples give a smoother trail and take longer to render. With 1 sample, no blur is drawn. ```swift highlight-motionBlur-samples // More samples give a smoother trail and take longer to render try engine.block.setInt(scene, property: "motionBlur/samples", value: 32) print(try engine.block.getInt(scene, property: "motionBlur/samples")) // 32 ``` ## Frame Rate The shutter angle refers to one frame of the video export. A video export without a frame rate option uses the scene property `scene/frameRate`, which is 30 by default. The preview uses the same frame. The `framerate` option of `VideoExportOptions` changes the length of one frame, so a higher frame rate draws a shorter trail. ```swift highlight-motionBlur-frameRate // One frame of the scene frame rate is the span the shutter angle refers to try engine.block.setFloat(scene, property: "scene/frameRate", value: 30) ``` ## Preview Motion Blur The preview draws the same blur as the video export. While the preview plays or changes, it draws fewer samples, at least 4, and smaller blur images to keep up. On a display that refreshes slower than the scene frame rate, it keeps up at the refresh rate of the display. It draws all samples once it settles. A block in crop or text editing draws sharp, so the edit shows. The `features/previewMotionBlurEnabled` setting is `true` by default. Set it to `false` to show the blocks sharp while editing. The video export ignores this setting, and the scene does not save it. See [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) for the other editor settings. ```swift highlight-motionBlur-previewSetting // The preview draws the motion blur by default. // Set the value to false to show the blocks sharp while editing. try engine.editor.setSettingBool("features/previewMotionBlurEnabled", value: false) print(try engine.editor.getSettingBool("features/previewMotionBlurEnabled")) // false ``` ## Handling Errors A shutter or samples value outside its range throws an `EngineError` whose `catalogCode` is `.blockMotionBlurValueOutOfRange`. The scene keeps the old value. ```swift highlight-motionBlur-errors do { try engine.block.setInt(scene, property: "motionBlur/samples", value: 100) } catch let error as EngineError { print(error.catalogCode == .blockMotionBlurValueOutOfRange) // true } ``` ## Exporting a Video `exportVideo(_:mimeType:options:onPreExport:uriResolver:)` draws the motion blur into every frame. An image export with `export(_:mimeType:)` draws the blocks sharp, so a single frame never shows the blur. ```swift highlight-motionBlur-exportVideo // Image export draws no motion blur. Export a video to see it in a file. let stream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in stream { if case let .finished(video: video) = event { try video.write(to: FileManager.default.temporaryDirectory.appendingPathComponent("motion-blur.mp4")) } } ``` ## Motion Blur and Other Operations - **Saving and loading** a scene keeps the block flag and the shutter values. - **Older scenes** load with motion blur off and the default shutter. - **Loaded values** outside their range are clamped to the range. - **Undo and redo** restore the block flag and the shutter values. ## Troubleshooting - **The block does not blur**: check that something moves the block during the shutter, that `motionBlur/samples` is greater than 1 and that `motionBlur/shutterAngle` is greater than 0. - **An exported image is sharp**: image export draws no motion blur. Export a video. - **The trail is too short**: raise `motionBlur/shutterAngle`, or export with a lower frame rate. - **The trail looks like separate copies**: raise `motionBlur/samples`. - **A value is out of range** (`.blockMotionBlurValueOutOfRange`): keep the angle from 0 to 720, the phase from -360 to 360 and the samples from 1 to 64. - **A value is not a number** (`.editorValueNotFinite`): pass a finite shutter angle and phase. - **The video export fails** (`.encodeVideoBlockHasError`): the trail of a block is too large to render on this device. Make the block or its motion smaller, or lower the export size. - **Setting the flag throws**: the block is not a graphic, text or caption block, or the `appearance/animation` scope is off. ## API Reference | Method | Description | | --- | --- | | `engine.block.setBool(_:property:value:)` | Turn `motionBlur/enabled` on or off for a block | | `engine.block.getBool(_:property:)` | Check whether a block has motion blur | | `engine.block.findAllProperties(_:)` | List the properties of a block, to check for `motionBlur/enabled` | | `engine.block.setFloat(_:property:value:)` | Set `motionBlur/shutterAngle`, `motionBlur/shutterPhase` or `scene/frameRate` | | `engine.block.setInt(_:property:value:)` | Set `motionBlur/samples` | | `engine.block.getInt(_:property:)` | Read the samples | | `engine.editor.setSettingBool(_:value:)` | Turn the preview motion blur on or off | | `engine.editor.getSettingBool(_:)` | Read the preview motion blur setting | | `engine.block.setKeyframes(_:property:keyframes:mode:)` | Move the block | | `engine.block.exportVideo(_:mimeType:options:onPreExport:uriResolver:)` | Export a video with motion blur | ## Next Steps - [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/): move blocks with keyframe tracks. - [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/): add preset animations that move blocks. - [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/): change other editor settings. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Motion Paths" description: "Move a block along a straight or curved path with vec2 position keyframes and spatial tangents." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/motion-paths-7c619f/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Motion Paths](https://img.ly/docs/cesdk/mac-catalyst/animation/motion-paths-7c619f/) --- ```swift file=@cesdk_swift_examples/engine-guides-motion-paths/MotionPaths.swift reference-only import Foundation import IMGLYEngine struct MotionPathsGuideResult { let ballPosition: SIMD2 let keyframeCount: Int let firstSpatialOut: SIMD2? let valueKindErrorCode: EngineErrorCode? let tangentErrorCode: EngineErrorCode? let pngData: Data } @MainActor func motionPaths(engine: Engine) async throws -> MotionPathsGuideResult { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) try engine.block.setDuration(page, duration: 5) // Creates a circle graphic of the given size and color on the page func createCircle(size: Float, color: Color) throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.ellipse)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: color) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: size) try engine.block.setHeight(block, value: size) try engine.block.appendChild(to: page, child: block) try engine.block.setDuration(block, duration: 5) return block } // Both tangents of a segment sit on its first keyframe. `spatialOut` is // relative to this keyframe's value, `spatialIn` to the next one's. let ball = try createCircle(size: 80, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) try engine.block.setVec2(ball, property: "position", value: [200, 800]) try engine.block.setKeyframes(ball, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([200, 800]), spatialOut: [0, -400], spatialIn: [-400, 0]), Keyframe(time: 0.5, value: .vec2([920, 300]), spatialOut: [400, 0], spatialIn: [0, -400]), Keyframe(time: 1, value: .vec2([1640, 800])), ]) let ballPosition = try engine.block.getVec2(ball, property: "position") print(ballPosition) // SIMD2(200.0, 800.0) let dot = try createCircle(size: 40, color: .rgba(r: 0.9, g: 0.3, b: 0.4, a: 1)) try engine.block.setVec2(dot, property: "position", value: [200, 980]) try engine.block.setKeyframes(dot, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([200, 980])), Keyframe(time: 1, value: .vec2([1680, 980])), ]) let keyframes = try engine.block.getKeyframes(ball, property: "position") print(keyframes.count, keyframes[0].spatialOut as Any) // 3 Optional(SIMD2(0.0, -400.0)) // The `position/y` track replaces the y-axis of the `position` track let hopper = try createCircle(size: 60, color: .rgba(r: 0.2, g: 0.7, b: 0.3, a: 1)) try engine.block.setKeyframes(hopper, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([200, 100])), Keyframe(time: 1, value: .vec2([1660, 100])), ]) try engine.block.setKeyframes(hopper, property: "position/y", keyframes: [ Keyframe(time: 0, value: .float(160)), Keyframe(time: 0.25, value: .float(40)), Keyframe(time: 0.5, value: .float(160)), Keyframe(time: 0.75, value: .float(40)), Keyframe(time: 1, value: .float(160)), ]) var valueKindErrorCode: EngineErrorCode? do { // A motion path takes vec2 values only try engine.block.setKeyframes(ball, property: "position", keyframes: [Keyframe(time: 0, value: .float(0))]) } catch let error as EngineError { valueKindErrorCode = error.catalogCode print(valueKindErrorCode == .blockKeyframesValueKindMismatch) // true } var tangentErrorCode: EngineErrorCode? do { // A single axis takes no spatial tangents try engine.block.setKeyframes(ball, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(0), spatialOut: [10, 0]), ]) } catch let error as EngineError { tangentErrorCode = error.catalogCode print(tangentErrorCode == .blockKeyframesSpatialTangentUnsupported) // true } // Export the frame at 1.25 seconds, while the ball climbs its path try engine.block.setPlaybackTime(page, time: 1.25) let pngData = try await engine.block.export(page, mimeType: .png) print(!pngData.isEmpty) // true try await engine.captureGuide(data: pngData, label: "hero", mimeType: .png) return MotionPathsGuideResult( ballPosition: ballPosition, keyframeCount: keyframes.count, firstSpatialOut: keyframes[0].spatialOut, valueKindErrorCode: valueKindErrorCode, tangentErrorCode: tangentErrorCode, pngData: pngData, ) } ``` Move a block along a straight or curved path with vec2 position keyframes and spatial tangents. ![A blue ball partway along its curve, a red dot partway across, and a green ball at the top of a hop, at 1.25 seconds](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-motion-paths) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. `position` is a vec2 property. One `.vec2` keyframe track moves the `/x` and `/y` properties of a block together, and spatial tangents curve the path between two keyframes. The editor has no motion path controls, so you set them with the block API. This guide covers how to curve a path with spatial tangents, how to move in a straight line, how to read a path back, how a single-axis track changes one axis, and the errors to expect. It ends with a frame export. ## Creating the Blocks The example draws its blocks as circles. A small local function creates each one on the page with a duration of 5 seconds. ```swift highlight-motionPaths-createCircle // Creates a circle graphic of the given size and color on the page func createCircle(size: Float, color: Color) throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.ellipse)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: color) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: size) try engine.block.setHeight(block, value: size) try engine.block.appendChild(to: page, child: block) try engine.block.setDuration(block, duration: 5) return block } ``` ## Curving a Path `setKeyframes(_:property:keyframes:mode:)` with `.vec2` keyframes sets the path of a block. Both tangents of a segment sit on its first keyframe. `spatialOut` leaves that keyframe's value and is relative to it. `spatialIn` enters the next keyframe's value and is relative to that value. The last keyframe uses neither. The ball leaves its start straight up, enters the top of its arc from the left, leaves it to the right, and enters its end from above. `setVec2(_:property:value:)` sets the static position, here at the start of the path, and `getVec2(_:property:)` reads it back. The keyframes replace the static position at every playback time. ```swift highlight-motionPaths-curvedPath // Both tangents of a segment sit on its first keyframe. `spatialOut` is // relative to this keyframe's value, `spatialIn` to the next one's. let ball = try createCircle(size: 80, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) try engine.block.setVec2(ball, property: "position", value: [200, 800]) try engine.block.setKeyframes(ball, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([200, 800]), spatialOut: [0, -400], spatialIn: [-400, 0]), Keyframe(time: 0.5, value: .vec2([920, 300]), spatialOut: [400, 0], spatialIn: [0, -400]), Keyframe(time: 1, value: .vec2([1640, 800])), ]) let ballPosition = try engine.block.getVec2(ball, property: "position") print(ballPosition) // SIMD2(200.0, 800.0) ``` ## Moving in a Straight Line Keyframes without tangents move a block in a straight line between their values. The dot slides along the bottom of the page. ```swift highlight-motionPaths-straightPath let dot = try createCircle(size: 40, color: .rgba(r: 0.9, g: 0.3, b: 0.4, a: 1)) try engine.block.setVec2(dot, property: "position", value: [200, 980]) try engine.block.setKeyframes(dot, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([200, 980])), Keyframe(time: 1, value: .vec2([1680, 980])), ]) ``` ## Reading the Path Back `getKeyframes(_:property:)` returns the keyframes of a property, sorted by time, with their tangents. ```swift highlight-motionPaths-readBack let keyframes = try engine.block.getKeyframes(ball, property: "position") print(keyframes.count, keyframes[0].spatialOut as Any) // 3 Optional(SIMD2(0.0, -400.0)) ``` ## Changing One Axis A `position/x` or `position/y` track with `.float` keyframes overrides that axis of a `position` track. The green ball moves right with its `position` track, while its `position/y` track makes it hop twice. ```swift highlight-motionPaths-axisOverride // The `position/y` track replaces the y-axis of the `position` track let hopper = try createCircle(size: 60, color: .rgba(r: 0.2, g: 0.7, b: 0.3, a: 1)) try engine.block.setKeyframes(hopper, property: "position", keyframes: [ Keyframe(time: 0, value: .vec2([200, 100])), Keyframe(time: 1, value: .vec2([1660, 100])), ]) try engine.block.setKeyframes(hopper, property: "position/y", keyframes: [ Keyframe(time: 0, value: .float(160)), Keyframe(time: 0.25, value: .float(40)), Keyframe(time: 0.5, value: .float(160)), Keyframe(time: 0.75, value: .float(40)), Keyframe(time: 1, value: .float(160)), ]) ``` ## Handling Errors A failed `setKeyframes(_:property:keyframes:mode:)` throws an `EngineError` whose `catalogCode` names the cause. The track stays unchanged. A `position` track takes vec2 values only, and only a vec2 keyframe takes spatial tangents. ```swift highlight-motionPaths-errors var valueKindErrorCode: EngineErrorCode? do { // A motion path takes vec2 values only try engine.block.setKeyframes(ball, property: "position", keyframes: [Keyframe(time: 0, value: .float(0))]) } catch let error as EngineError { valueKindErrorCode = error.catalogCode print(valueKindErrorCode == .blockKeyframesValueKindMismatch) // true } var tangentErrorCode: EngineErrorCode? do { // A single axis takes no spatial tangents try engine.block.setKeyframes(ball, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(0), spatialOut: [10, 0]), ]) } catch let error as EngineError { tangentErrorCode = error.catalogCode print(tangentErrorCode == .blockKeyframesSpatialTangentUnsupported) // true } ``` ## Exporting a Frame We export the page as PNG at 1.25 seconds. The frame shows the blue ball on the rising part of its arc, the red dot a quarter of the way along the bottom, and the green ball at the top of its first hop. ```swift highlight-motionPaths-export // Export the frame at 1.25 seconds, while the ball climbs its path try engine.block.setPlaybackTime(page, time: 1.25) let pngData = try await engine.block.export(page, mimeType: .png) print(!pngData.isEmpty) // true ``` ## Motion Paths and Other Block Operations - Position keyframes are in design units, also when the position mode of the block is `.percent`. - Scaling the block or changing the design unit also converts its position keyframes. - A `.relative` `position` track adds its offsets to the static position, so the path moves with the block. For relative tracks and easing, see [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/). - `shape/position` places a subshape with the same vec2 keyframes and tangents. - When the engine enforces scopes, setting keyframes needs the `appearance/animation` scope. A `position`, `position/x` or `position/y` track also needs `layer/move`, and a `shape/position` track needs `shape/change`. ## Troubleshooting - **The path does not take the values** (`EngineErrorCode.blockKeyframesValueKindMismatch`): pass vec2 keyframes for `position` and `shape/position`. - **The tangents are refused** (`EngineErrorCode.blockKeyframesSpatialTangentUnsupported`): set `spatialOut` and `spatialIn` only on vec2 keyframes, not on `position/x` or `position/y`. - **The block does not follow the curve**: put both tangents of a segment on its first keyframe. A `spatialIn` on the last keyframe has no effect. - **One axis ignores the path**: a `position/x` or `position/y` track overrides that axis. Remove it to use the path on both axes. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.create(_:)` | Create a graphic block | | `engine.block.createShape(_:)` | Create the ellipse shape | | `engine.block.setShape(_:shape:)` | Give a graphic its shape | | `engine.block.createFill(_:)` | Create a color fill | | `engine.block.setColor(_:property:color:)` | Set the fill color | | `engine.block.setFill(_:fill:)` | Give a graphic its fill | | `engine.block.setWidth(_:value:)` | Set the width of a block | | `engine.block.setHeight(_:value:)` | Set the height of a block | | `engine.block.appendChild(to:child:)` | Add a block to the page | | `engine.block.setDuration(_:duration:)` | Set the duration that keyframe times refer to | | `engine.block.setKeyframes(_:property:keyframes:mode:)` | Set the path or a single-axis track | | `engine.block.getKeyframes(_:property:)` | Read the keyframes and tangents of a property | | `engine.block.setVec2(_:property:value:)` | Set the static position | | `engine.block.getVec2(_:property:)` | Read the static position | | `engine.block.setPlaybackTime(_:time:)` | Seek before you export a frame | | `engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:)` | Export the frame as PNG | ### Keyframe Fields | Field | Type | Description | | --- | --- | --- | | `spatialOut` | `SIMD2?` | Tangent that leaves this keyframe's value, relative to it | | `spatialIn` | `SIMD2?` | Tangent that enters the next keyframe's value, relative to that value. It sits on this keyframe | ## Next Steps - [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/): easing, relative tracks and the other keyframe value kinds. - [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/): add preset animations to blocks. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Overview" description: "Add motion to video scenes with preset animation controls and programmatic animation APIs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/overview-6a2ef2/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/animation/overview-6a2ef2/) --- Animations in CreativeEditor SDK (CE.SDK) bring your designs to life by adding motion to images, text, and design elements in video scenes. Whether you're creating a dynamic social media post, a video ad, or an engaging product demo, animations help capture attention and communicate ideas more effectively. The editor UI can expose preset in, out, and loop animations for selected video, image, sticker, shape, and text blocks. You can adjust the properties exposed by each preset in the UI where available, or control animations programmatically with the CreativeEngine API. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## Animation Capabilities in CE.SDK CE.SDK enables animation across a variety of design elements, giving you the flexibility to animate: - **Images:** Animate image blocks with movements like fades, zooms, or rotations. - **Text:** Animate text layers to create effects such as typewriter reveals or slide-ins. - **Shapes and Graphics:** Add motion to vector shapes, icons, and graphic blocks to create visually rich layouts. You can animate key properties of these elements, including: - **Position:** Move elements across the canvas. - **Scale:** Zoom in or out dynamically. - **Rotation:** Spin or pivot elements over time. - **Opacity:** Fade elements in and out. ## Supported Animation Types CE.SDK provides a range of animation types designed for common motion effects. Core categories include: - **Fade:** Smooth transitions in or out using opacity changes. - **Slide:** Move elements into or out of the frame from any direction. - **Zoom:** Scale elements up or down to create dynamic emphasis. - **Rotate:** Apply rotational motion for spins or turns. These animations can be used as in, out or loop animations to create complex sequences. ## Timeline and Preset Timing Animations in CE.SDK are time-based presets attached to blocks in video scenes. In, out, and loop animations start relative to block visibility on the page timeline. In animations play when a block appears, out animations play when it leaves, and loop animations repeat while the block is visible. These preset animations cover common motion. To animate a single property with your own values, times and easing curves, use [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/). Use the editor UI for supported preset selection and property adjustments, or use CreativeEngine for programmatic setup and rendering. Use MP4 export when you need to preserve motion in the final output. ## Next Steps - [Supported Animation Types](https://img.ly/docs/cesdk/mac-catalyst/animation/types-4e5f41/) — Compare available object and text animation presets and their properties. - [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/) — Build entrance, exit, loop, and text animations in one end-to-end workflow. - [Edit Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/edit-32c12a/) — Inspect, update, replace, or remove animations already attached to a block. - [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/) — Animate single block properties with your own values, times and easing curves. - [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) — Arrange video tracks, clips, and audio on a time-based canvas. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Text Animators" description: "Animate the characters, words or lines of a text block with text animators, range selectors and keyframes." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/text-animators-97387e/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Text Animators](https://img.ly/docs/cesdk/mac-catalyst/animation/text-animators-97387e/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-animators/TextAnimators.swift reference-only import Foundation import IMGLYEngine struct TextAnimatorsGuideResult { let textSupportsAnimators: Bool let pageSupportsAnimators: Bool let reveal: DesignBlockID let highlight: DesignBlockID let wave: DesignBlockID let orderAfterInsert: [DesignBlockID] let highlightIsValidAfterRemove: Bool let orderAfterReappend: [DesignBlockID] let countdownKeyframeCount: Int let errorCode: EngineErrorCode? let pngData: Data } @MainActor func textAnimators(engine: Engine) async throws -> TextAnimatorsGuideResult { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) // A 5 second headline in the middle of the page let text = try engine.block.create(.text) try engine.block.replaceText(text, text: "Text animators move every letter") try engine.block.setTextFontSize(text, fontSize: 120) try engine.block.setEnum(text, property: "text/horizontalAlignment", value: "Center") try engine.block.setWidth(text, value: 1600) try engine.block.setHeightMode(text, mode: .auto) try engine.block.setPositionX(text, value: 160) try engine.block.setPositionY(text, value: 380) try engine.block.appendChild(to: page, child: text) try engine.block.setDuration(text, duration: 5) let textSupportsAnimators = try engine.block.supportsTextAnimators(text) // true let pageSupportsAnimators = try engine.block.supportsTextAnimators(page) // false // The covered glyphs sit 60 units lower and are hidden let reveal = try engine.block.createTextAnimator() try engine.block.setFloat(reveal, property: "text_animator/positionY", value: 60) try engine.block.setBool(reveal, property: "text_animator/opacityEnabled", value: true) try engine.block.setFloat(reveal, property: "text_animator/opacity", value: 0) try engine.block.appendTextAnimator(text, animatorID: reveal) // Move the start of the range from 0 to 1 in the first half of the // text's duration. The uncovered glyphs rise and fade in, left to right. try engine.block.setKeyframes(reveal, property: "text_animator/selectorStart", keyframes: [ Keyframe(time: 0, value: .float(0), easing: .preset(.easeOut)), Keyframe(time: 0.5, value: .float(1)), ]) // Cover the second word only: words are counted from 0 in index units let highlight = try engine.block.createTextAnimator() try engine.block.setEnum(highlight, property: "text_animator/selectorBasedOn", value: "Words") try engine.block.setEnum(highlight, property: "text_animator/selectorUnits", value: "Index") try engine.block.setFloat(highlight, property: "text_animator/selectorStart", value: 1) try engine.block.setFloat(highlight, property: "text_animator/selectorEnd", value: 2) // Color and scale the covered glyphs try engine.block.setBool(highlight, property: "text_animator/fillColorEnabled", value: true) try engine.block.setColor( highlight, property: "text_animator/fillColor", color: .rgba(r: 1, g: 0.5, b: 0, a: 1), ) try engine.block.setFloat(highlight, property: "text_animator/scaleX", value: 1.2) try engine.block.setFloat(highlight, property: "text_animator/scaleY", value: 1.2) // Insert the highlight before the reveal try engine.block.insertTextAnimator(text, animatorID: highlight, index: 0) let orderAfterInsert = try engine.block.getTextAnimators(text) // [highlight, reveal] // A triangle covers a quarter of the characters and peaks in its middle let wave = try engine.block.createTextAnimator() try engine.block.setEnum(wave, property: "text_animator/selectorShape", value: "Triangle") try engine.block.setFloat(wave, property: "text_animator/selectorEnd", value: 0.25) try engine.block.setFloat(wave, property: "text_animator/positionY", value: -40) try engine.block.setFloat(wave, property: "text_animator/rotation", value: -0.2) // Move the range across the text over the whole duration try engine.block.setKeyframes(wave, property: "text_animator/selectorOffset", keyframes: [ Keyframe(time: 0, value: .float(-0.25)), Keyframe(time: 1, value: .float(1)), ]) try engine.block.appendTextAnimator(text, animatorID: wave) // Remove the highlight at index 0. The animator stays valid. try engine.block.removeTextAnimator(text, index: 0) let highlightIsValidAfterRemove = engine.block.isValid(highlight) // true // Append it again so that it applies last try engine.block.appendTextAnimator(text, animatorID: highlight) let orderAfterReappend = try engine.block.getTextAnimators(text) // [reveal, wave, highlight] print(orderAfterInsert, highlightIsValidAfterRemove, orderAfterReappend) // A second text block counts down from 3 to 1 let countdown = try engine.block.create(.text) try engine.block.replaceText(countdown, text: "3") try engine.block.setTextFontSize(countdown, fontSize: 160) try engine.block.setWidthMode(countdown, mode: .auto) try engine.block.setHeightMode(countdown, mode: .auto) try engine.block.setPositionX(countdown, value: 900) try engine.block.setPositionY(countdown, value: 700) try engine.block.appendChild(to: page, child: countdown) try engine.block.setDuration(countdown, duration: 5) try engine.block.setKeyframes(countdown, property: "text/text", keyframes: [ Keyframe(time: 0, value: .discrete(.string("3"))), Keyframe(time: 0.33, value: .discrete(.string("2"))), Keyframe(time: 0.67, value: .discrete(.string("1"))), ]) var errorCode: EngineErrorCode? do { // The reveal already belongs to the headline try engine.block.appendTextAnimator(countdown, animatorID: reveal) } catch let error as EngineError { errorCode = error.catalogCode print(errorCode == .blockTextAnimatorAlreadyAttached) // true } // Export the frame at 2 seconds, during the reveal try engine.block.setPlaybackTime(page, time: 2) let pngData = try await engine.block.export(page, mimeType: .png) let countdownKeyframeCount = try engine.block.getKeyframes(countdown, property: "text/text").count return TextAnimatorsGuideResult( textSupportsAnimators: textSupportsAnimators, pageSupportsAnimators: pageSupportsAnimators, reveal: reveal, highlight: highlight, wave: wave, orderAfterInsert: orderAfterInsert, highlightIsValidAfterRemove: highlightIsValidAfterRemove, orderAfterReappend: orderAfterReappend, countdownKeyframeCount: countdownKeyframeCount, errorCode: errorCode, pngData: pngData, ) } ``` Animate the characters, words or lines of a text block with text animators, such as a letter-by-letter reveal, a highlighted word or a wave. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-text-animators) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Text presets animate a whole text with fixed settings. A text animator gives you control over each glyph. It selects a range of characters, words or lines and changes their position, scale, rotation, skew, opacity, colors, stroke width, spacing and blur. Every property of a text animator accepts keyframes, so the range and the changes can move over time. Text animators render in playback and in video export. The editor has no text animator controls, so you set them with the block API. For text presets, see [Text Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/text-d6f4aa/). This guide covers creating and attaching text animators, the glyph properties and the range selector. It also covers keyframes, the order of several animators, removing them, keyframes on the text content, and errors. ## Creating the Text Block We create a headline with a duration of 5 seconds. The keyframes of its text animators use this duration. ```swift highlight-textAnimators-createText // A 5 second headline in the middle of the page let text = try engine.block.create(.text) try engine.block.replaceText(text, text: "Text animators move every letter") try engine.block.setTextFontSize(text, fontSize: 120) try engine.block.setEnum(text, property: "text/horizontalAlignment", value: "Center") try engine.block.setWidth(text, value: 1600) try engine.block.setHeightMode(text, mode: .auto) try engine.block.setPositionX(text, value: 160) try engine.block.setPositionY(text, value: 380) try engine.block.appendChild(to: page, child: text) try engine.block.setDuration(text, duration: 5) ``` ## Creating a Text Animator `supportsTextAnimators(_:)` returns `true` for text and caption blocks. `createTextAnimator()` creates a text animator, and `appendTextAnimator(_:animatorID:)` attaches it to the end of the text's animator list. ```swift highlight-textAnimators-createAnimator let textSupportsAnimators = try engine.block.supportsTextAnimators(text) // true let pageSupportsAnimators = try engine.block.supportsTextAnimators(page) // false // The covered glyphs sit 60 units lower and are hidden let reveal = try engine.block.createTextAnimator() try engine.block.setFloat(reveal, property: "text_animator/positionY", value: 60) try engine.block.setBool(reveal, property: "text_animator/opacityEnabled", value: true) try engine.block.setFloat(reveal, property: "text_animator/opacity", value: 0) try engine.block.appendTextAnimator(text, animatorID: reveal) ``` All properties of a text animator start with `text_animator/`. A new text animator changes nothing, because its offsets are 0 and its scale is 1. This animator moves the glyphs 60 units down and hides them. The range selector decides which glyphs get these changes. By default it covers the whole text. | Property | Unit | Effect on a covered glyph | | --- | --- | --- | | `positionX`, `positionY` | Design units | Moves the glyph | | `anchorX`, `anchorY` | Design units | Moves the pivot for scale, rotation and skew | | `scaleX`, `scaleY` | Factor, 1 is no change | Scales the glyph | | `rotation` | Radians | Rotates the glyph | | `skew`, `skewAxis` | Radians | Skews the glyph along an axis | | `opacity` | 0 to 1 | Fades the glyph when `opacityEnabled` is `true` | | `fillColor` | Color | Colors the glyph when `fillColorEnabled` is `true` | | `strokeColor` | Color | Colors the stroke when `strokeColorEnabled` is `true` | | `strokeWidth` | Design units | Adds to the stroke width when the text has a stroke | | `tracking` | Design units | Adds space, half before and half after the glyph | | `blurX`, `blurY` | Design units | Blurs the glyph | ## Animating the Range with Keyframes Every text animator property accepts keyframes with `setKeyframes(_:property:keyframes:mode:)`. The keyframe times refer to the duration of the text block. Here `selectorStart` moves from 0 to 1 in the first 2.5 seconds. The range then covers fewer glyphs, from the left. Each glyph that leaves the range rises to its place and fades in. ```swift highlight-textAnimators-keyframedReveal // Move the start of the range from 0 to 1 in the first half of the // text's duration. The uncovered glyphs rise and fade in, left to right. try engine.block.setKeyframes(reveal, property: "text_animator/selectorStart", keyframes: [ Keyframe(time: 0, value: .float(0), easing: .preset(.easeOut)), Keyframe(time: 0.5, value: .float(1)), ]) ``` For more on keyframe values and easing, see [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/). ## The Range Selector The range selector gives each glyph a coverage from 0 to 1. A glyph with full coverage gets the full change of the animator. These properties control the selector: - `selectorBasedOn` is the element that the selector counts: `"Characters"`, `"CharactersExcludingSpaces"`, `"Words"` or `"Lines"`. - `selectorUnits` is `"Percentage"` by default, where 0 to 1 spans all elements. `"Index"` counts elements from 0. - `selectorStart`, `selectorEnd` and `selectorOffset` define the range. The offset moves the whole range. - `selectorShape` is the coverage curve over the range: `"Square"`, `"RampUp"`, `"RampDown"`, `"Triangle"`, `"Round"` or `"Smooth"`. `selectorSmoothness` softens the edges of a square. - `selectorAmount` scales the coverage, from -1 to 1. `selectorEaseHigh` and `selectorEaseLow` bend the curve. - `selectorRandomize` shuffles the order of the elements. The same `selectorSeed` gives the same order on every platform. - `selectorEnabled` set to `false` covers every glyph. This animator covers the second word, colors it orange and scales it up. ```swift highlight-textAnimators-rangeSelector // Cover the second word only: words are counted from 0 in index units let highlight = try engine.block.createTextAnimator() try engine.block.setEnum(highlight, property: "text_animator/selectorBasedOn", value: "Words") try engine.block.setEnum(highlight, property: "text_animator/selectorUnits", value: "Index") try engine.block.setFloat(highlight, property: "text_animator/selectorStart", value: 1) try engine.block.setFloat(highlight, property: "text_animator/selectorEnd", value: 2) // Color and scale the covered glyphs try engine.block.setBool(highlight, property: "text_animator/fillColorEnabled", value: true) try engine.block.setColor( highlight, property: "text_animator/fillColor", color: .rgba(r: 1, g: 0.5, b: 0, a: 1), ) try engine.block.setFloat(highlight, property: "text_animator/scaleX", value: 1.2) try engine.block.setFloat(highlight, property: "text_animator/scaleY", value: 1.2) ``` In index units the default `selectorEnd` of 1 covers only the first element. Set the end to the index after the last element that you want to cover. ## Combining Several Animators A text block can have several text animators. They apply in list order, and each adds its change to the result of the animators before it. `insertTextAnimator(_:animatorID:index:)` attaches an animator at an index, and `getTextAnimators(_:)` returns the list. ```swift highlight-textAnimators-stackOrder // Insert the highlight before the reveal try engine.block.insertTextAnimator(text, animatorID: highlight, index: 0) let orderAfterInsert = try engine.block.getTextAnimators(text) // [highlight, reveal] ``` A third animator runs a wave across the text. The triangle shape covers a quarter of the characters and peaks in its middle. A keyframed `selectorOffset` moves this range from before the first character to after the last one. ```swift highlight-textAnimators-wave // A triangle covers a quarter of the characters and peaks in its middle let wave = try engine.block.createTextAnimator() try engine.block.setEnum(wave, property: "text_animator/selectorShape", value: "Triangle") try engine.block.setFloat(wave, property: "text_animator/selectorEnd", value: 0.25) try engine.block.setFloat(wave, property: "text_animator/positionY", value: -40) try engine.block.setFloat(wave, property: "text_animator/rotation", value: -0.2) // Move the range across the text over the whole duration try engine.block.setKeyframes(wave, property: "text_animator/selectorOffset", keyframes: [ Keyframe(time: 0, value: .float(-0.25)), Keyframe(time: 1, value: .float(1)), ]) try engine.block.appendTextAnimator(text, animatorID: wave) ``` ## Removing a Text Animator `removeTextAnimator(_:index:)` removes the animator at an index. It does not destroy the animator, so you can attach it again or destroy it with `destroy(_:)`. To change the order, remove an animator and insert it at the new index. ```swift highlight-textAnimators-remove // Remove the highlight at index 0. The animator stays valid. try engine.block.removeTextAnimator(text, index: 0) let highlightIsValidAfterRemove = engine.block.isValid(highlight) // true // Append it again so that it applies last try engine.block.appendTextAnimator(text, animatorID: highlight) let orderAfterReappend = try engine.block.getTextAnimators(text) // [reveal, wave, highlight] print(orderAfterInsert, highlightIsValidAfterRemove, orderAfterReappend) ``` ## Animating the Text Content The `text/text` property of a text block accepts `.discrete(.string(...))` keyframes. The shown text changes at the time of each keyframe. This countdown shows 3, then 2, then 1. ```swift highlight-textAnimators-textContent // A second text block counts down from 3 to 1 let countdown = try engine.block.create(.text) try engine.block.replaceText(countdown, text: "3") try engine.block.setTextFontSize(countdown, fontSize: 160) try engine.block.setWidthMode(countdown, mode: .auto) try engine.block.setHeightMode(countdown, mode: .auto) try engine.block.setPositionX(countdown, value: 900) try engine.block.setPositionY(countdown, value: 700) try engine.block.appendChild(to: page, child: countdown) try engine.block.setDuration(countdown, duration: 5) try engine.block.setKeyframes(countdown, property: "text/text", keyframes: [ Keyframe(time: 0, value: .discrete(.string("3"))), Keyframe(time: 0.33, value: .discrete(.string("2"))), Keyframe(time: 0.67, value: .discrete(.string("1"))), ]) ``` The keyframed text uses the style at the start of the static text. `getString(_:property:)` and editing in the canvas use the static text, not the keyframed one. Caption blocks accept the same keyframes on `caption/text`. ## Handling Errors Invalid input throws an `EngineError`. Its `catalogCode` names the problem. A text animator belongs to one text block at a time, so attaching it to a second block fails. ```swift highlight-textAnimators-errors var errorCode: EngineErrorCode? do { // The reveal already belongs to the headline try engine.block.appendTextAnimator(countdown, animatorID: reveal) } catch let error as EngineError { errorCode = error.catalogCode print(errorCode == .blockTextAnimatorAlreadyAttached) // true } ``` ## Exporting a Frame We set the page's playback time to 2 seconds and export the page as PNG. At that time most of the headline has risen into place, the wave lifts the letters of the second quarter, and the countdown shows 2. ```swift highlight-textAnimators-export // Export the frame at 2 seconds, during the reveal try engine.block.setPlaybackTime(page, time: 2) let pngData = try await engine.block.export(page, mimeType: .png) ``` ## Text Animators and Other Block Operations Text animators take part in the usual block operations: - **Preset animations** apply together with text animators. - **Undo and redo** restore the animators of a text and their properties. - **Saving and loading** a scene keeps the animators and their keyframes. - **Duplicating** a text block copies its animators in their order. - **Destroying** a text block destroys its animators. Destroying an animator removes it from its text block. - **Changing the design unit** converts the length properties of an animator, such as `tracking`. Factors such as `scaleX` stay the same. `appendTextAnimator(_:animatorID:)`, `insertTextAnimator(_:animatorID:index:)` and `removeTextAnimator(_:index:)` need the `appearance/animation` scope. > **Caution:** Text on a path does not show text animators yet. Scenes that contain text animators do not open in earlier versions of CE.SDK. ## Troubleshooting - **The block does not accept text animators** (`.blockTextAnimatorsUnsupported`): use a text or caption block. Check it with `supportsTextAnimators(_:)`. - **The block to attach is not a text animator** (`.blockTextAnimatorNotAnAnimator`): create the block with `createTextAnimator()`. - **The animator already belongs to a block** (`.blockTextAnimatorAlreadyAttached`): remove it from its text block first, or create a new animator. - **The index is out of bounds** (`.blockTextAnimatorIndexOutOfBounds`): to insert, use an index from 0 to the length of `getTextAnimators(_:)`. To remove, use a lower index. - **Only the first character changes**: in `"Index"` units the default end of 1 covers only element 0. Set `selectorEnd`. - **The opacity or a color does not change**: set `opacityEnabled`, `fillColorEnabled` or `strokeColorEnabled` to `true`. - **The stroke width does not change**: enable the stroke of the text block. - **A negative amount does not fade the glyphs**: a negative coverage moves, scales and rotates glyphs, but it does not change opacity or colors. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.supportsTextAnimators(_:)` | Check whether a block accepts text animators | | `engine.block.createTextAnimator()` | Create a text animator | | `engine.block.appendTextAnimator(_:animatorID:)` | Attach an animator at the end of the list | | `engine.block.insertTextAnimator(_:animatorID:index:)` | Attach an animator at an index | | `engine.block.getTextAnimators(_:)` | Get the animators of a block in the order they apply | | `engine.block.removeTextAnimator(_:index:)` | Remove the animator at an index without destroying it | | `engine.block.isValid(_:)` | Check that a removed animator still exists | | `engine.block.setFloat(_:property:value:)` | Set a number property of a text animator | | `engine.block.setInt(_:property:value:)` | Set an integer property of a text animator, such as `selectorSeed` | | `engine.block.setBool(_:property:value:)` | Turn the opacity or a color of a text animator on | | `engine.block.setEnum(_:property:value:)` | Set the selector shape, units or elements, or the text alignment | | `engine.block.setColor(_:property:color:)` | Set the fill or stroke color of a text animator | | `engine.block.setKeyframes(_:property:keyframes:mode:)` | Animate a text animator property or the text content | | `engine.block.create(_:)` | Create the text blocks | | `engine.block.replaceText(_:text:in:)` | Set the static text | | `engine.block.setTextFontSize(_:fontSize:in:)` | Set the font size | | `engine.block.setWidth(_:value:)` | Set the text block width | | `engine.block.setWidthMode(_:mode:)` | Let the width follow the text | | `engine.block.setHeightMode(_:mode:)` | Let the height follow the text | | `engine.block.setPositionX(_:value:)` | Set the x position | | `engine.block.setPositionY(_:value:)` | Set the y position | | `engine.block.appendChild(to:child:)` | Add the text block to the page | | `engine.block.setDuration(_:duration:)` | Set the duration that keyframe times refer to | | `engine.block.setPlaybackTime(_:time:)` | Set the time to render | | `engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:)` | Export the page at the current playback time | ## Next Steps - [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/): animate single block properties with keyframes and easing. - [Text Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/text-d6f4aa/): apply preset animations to text blocks. - [Animation Overview](https://img.ly/docs/cesdk/mac-catalyst/animation/overview-6a2ef2/): understand animation concepts and capabilities. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Time Remap" description: "Freeze, reverse or replay the children of a composition group by remapping their clock with a static value or keyframes." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/time-remap-063ab0/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Time Remap](https://img.ly/docs/cesdk/mac-catalyst/animation/time-remap-063ab0/) --- ```swift file=@cesdk_swift_examples/engine-guides-time-remap/TimeRemap.swift reference-only import Foundation import IMGLYEngine struct TimeRemapGuideResult { let normalRemapEnabled: Bool let reversedStaticRemap: Float let remapIsAnimatable: Bool let errorCode: EngineErrorCode? let pngData: Data } @MainActor func timeRemap(engine: Engine) async throws -> TimeRemapGuideResult { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) try engine.block.setDuration(page, duration: 5) // Creates a lane: a composition group with a line and a ball that crosses // the page in 5 seconds of the group's own clock func createLane(y: Float, color: Color) throws -> DesignBlockID { let line = try engine.block.create(.graphic) try engine.block.setShape(line, shape: engine.block.createShape(.rect)) let lineFill = try engine.block.createFill(.color) try engine.block.setColor(lineFill, property: "fill/color/value", color: .rgba(r: 0.85, g: 0.85, b: 0.85, a: 1)) try engine.block.setFill(line, fill: lineFill) try engine.block.setWidth(line, value: 1680) try engine.block.setHeight(line, value: 6) try engine.block.setPositionX(line, value: 120) try engine.block.setPositionY(line, value: y + 37) try engine.block.appendChild(to: page, child: line) try engine.block.setDuration(line, duration: 5) let ball = try engine.block.create(.graphic) try engine.block.setShape(ball, shape: engine.block.createShape(.ellipse)) let ballFill = try engine.block.createFill(.color) try engine.block.setColor(ballFill, property: "fill/color/value", color: color) try engine.block.setFill(ball, fill: ballFill) try engine.block.setWidth(ball, value: 80) try engine.block.setHeight(ball, value: 80) try engine.block.setPositionY(ball, value: y) try engine.block.appendChild(to: page, child: ball) try engine.block.setDuration(ball, duration: 5) try engine.block.setKeyframes(ball, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(120)), Keyframe(time: 1, value: .float(1720)), ]) let lane = try engine.block.group([line, ball]) try engine.block.setDuration(lane, duration: 5) // A composition group gives its children their own clock try engine.block.setBool(lane, property: "group/composition", value: true) return lane } let normal = try createLane(y: 150, color: .rgba(r: 0.6, g: 0.6, b: 0.6, a: 1)) let frozen = try createLane(y: 400, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) let reversed = try createLane(y: 650, color: .rgba(r: 0.9, g: 0.3, b: 0.4, a: 1)) let pingPong = try createLane(y: 900, color: .rgba(r: 0.2, g: 0.7, b: 0.3, a: 1)) let normalRemapEnabled = try engine.block.getBool(normal, property: "playback/timeRemapEnabled") print(normalRemapEnabled) // false // A static remap holds the children at 2.5 seconds of their own time try engine.block.setBool(frozen, property: "playback/timeRemapEnabled", value: true) try engine.block.setFloat(frozen, property: "playback/timeRemap", value: 2.5) // The track values are times on the children's clock, in seconds. They // stay below 5, because a clock at a child's end hides the child. try engine.block.setBool(reversed, property: "playback/timeRemapEnabled", value: true) try engine.block.setKeyframes(reversed, property: "playback/timeRemap", keyframes: [ Keyframe(time: 0, value: .float(4.99)), Keyframe(time: 1, value: .float(0)), ]) try engine.block.setBool(pingPong, property: "playback/timeRemapEnabled", value: true) try engine.block.setKeyframes(pingPong, property: "playback/timeRemap", keyframes: [ Keyframe(time: 0, value: .float(0), easing: .preset(.easeInOut)), Keyframe(time: 0.5, value: .float(4.99), easing: .preset(.easeInOut)), Keyframe(time: 1, value: .float(0)), ]) // The track leaves the static remap unchanged let reversedStaticRemap = try engine.block.getFloat(reversed, property: "playback/timeRemap") let remapIsAnimatable = try engine.block.isPropertyAnimatable(reversed, property: "playback/timeRemap") print(reversedStaticRemap, remapIsAnimatable) // 0.0 true var errorCode: EngineErrorCode? do { // Only a group has a time remap if let ball = try engine.block.getChildren(normal).last { try engine.block.setFloat(ball, property: "playback/timeRemap", value: 1) } } catch let error as EngineError { errorCode = error.catalogCode print(errorCode == .editorBlockNoTimeRemapSupport) // true } // Export the frame at 1.5 seconds try engine.block.setPlaybackTime(page, time: 1.5) let pngData = try await engine.block.export(page, mimeType: .png) print(!pngData.isEmpty) // true try await engine.captureGuide(data: pngData, label: "hero", mimeType: .png) return TimeRemapGuideResult( normalRemapEnabled: normalRemapEnabled, reversedStaticRemap: reversedStaticRemap, remapIsAnimatable: remapIsAnimatable, errorCode: errorCode, pngData: pngData, ) } ``` Freeze, reverse or replay the children of a composition group by remapping their clock. ![Four lanes of the same moving ball: normal, frozen, reversed and forward and back, at 1.5 seconds](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-time-remap) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. A composition group gives its children their own clock. `playback/timeRemap` sets the time on that clock, in seconds, that the children show. A static value freezes them, and `.float` keyframes drive them over the group's duration. The editor has no time remap controls, so you set them with the block API. This guide covers how to turn time remap on, how to freeze the children, how to play them in reverse or forward and back, how to read the remap back, and the error to expect. It ends with a frame export. ## Creating a Composition Group Time remap acts on the children of a composition group. Each lane of the example is a group with a gray line and a ball on `page`, the 5-second page of the scene. The ball has a `position/x` track and crosses the page in 5 seconds of the group's clock. `group(_:)` groups the two blocks, and `group/composition` gives the group its own clock for its children. ```swift highlight-timeRemap-createLane // Creates a lane: a composition group with a line and a ball that crosses // the page in 5 seconds of the group's own clock func createLane(y: Float, color: Color) throws -> DesignBlockID { let line = try engine.block.create(.graphic) try engine.block.setShape(line, shape: engine.block.createShape(.rect)) let lineFill = try engine.block.createFill(.color) try engine.block.setColor(lineFill, property: "fill/color/value", color: .rgba(r: 0.85, g: 0.85, b: 0.85, a: 1)) try engine.block.setFill(line, fill: lineFill) try engine.block.setWidth(line, value: 1680) try engine.block.setHeight(line, value: 6) try engine.block.setPositionX(line, value: 120) try engine.block.setPositionY(line, value: y + 37) try engine.block.appendChild(to: page, child: line) try engine.block.setDuration(line, duration: 5) let ball = try engine.block.create(.graphic) try engine.block.setShape(ball, shape: engine.block.createShape(.ellipse)) let ballFill = try engine.block.createFill(.color) try engine.block.setColor(ballFill, property: "fill/color/value", color: color) try engine.block.setFill(ball, fill: ballFill) try engine.block.setWidth(ball, value: 80) try engine.block.setHeight(ball, value: 80) try engine.block.setPositionY(ball, value: y) try engine.block.appendChild(to: page, child: ball) try engine.block.setDuration(ball, duration: 5) try engine.block.setKeyframes(ball, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(120)), Keyframe(time: 1, value: .float(1720)), ]) let lane = try engine.block.group([line, ball]) try engine.block.setDuration(lane, duration: 5) // A composition group gives its children their own clock try engine.block.setBool(lane, property: "group/composition", value: true) return lane } ``` ## Creating the Lanes The example creates four lanes. The first one plays normally, and the other three get a time remap below. `playback/timeRemapEnabled` is off by default, and `getBool(_:property:)` reads it. ```swift highlight-timeRemap-lanes let normal = try createLane(y: 150, color: .rgba(r: 0.6, g: 0.6, b: 0.6, a: 1)) let frozen = try createLane(y: 400, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) let reversed = try createLane(y: 650, color: .rgba(r: 0.9, g: 0.3, b: 0.4, a: 1)) let pingPong = try createLane(y: 900, color: .rgba(r: 0.2, g: 0.7, b: 0.3, a: 1)) let normalRemapEnabled = try engine.block.getBool(normal, property: "playback/timeRemapEnabled") print(normalRemapEnabled) // false ``` ## Freezing the Children `setBool(_:property:value:)` turns on `playback/timeRemapEnabled`. A static `playback/timeRemap`, set with `setFloat(_:property:value:)`, then holds the children at that time on their clock, here 2.5 seconds, so the blue ball stays in the middle of its lane. ```swift highlight-timeRemap-freeze // A static remap holds the children at 2.5 seconds of their own time try engine.block.setBool(frozen, property: "playback/timeRemapEnabled", value: true) try engine.block.setFloat(frozen, property: "playback/timeRemap", value: 2.5) ``` ## Playing in Reverse A track on `playback/timeRemap` drives the children while the group plays. The keyframe times are normalized over the group's duration, from 0 to 1. The values are times on the children's clock, in seconds. The track from just below 5 to 0 plays the red ball from the end of its lane back to the start. The values stay below 5, because a child shows only while the clock is before its end. ```swift highlight-timeRemap-reverse // The track values are times on the children's clock, in seconds. They // stay below 5, because a clock at a child's end hides the child. try engine.block.setBool(reversed, property: "playback/timeRemapEnabled", value: true) try engine.block.setKeyframes(reversed, property: "playback/timeRemap", keyframes: [ Keyframe(time: 0, value: .float(4.99)), Keyframe(time: 1, value: .float(0)), ]) ``` ## Playing Forward and Back Three keyframes play the children forward to just below 5 seconds and back to 0. The `.easeInOut` easing slows the green ball at both ends and at the turn. ```swift highlight-timeRemap-pingPong try engine.block.setBool(pingPong, property: "playback/timeRemapEnabled", value: true) try engine.block.setKeyframes(pingPong, property: "playback/timeRemap", keyframes: [ Keyframe(time: 0, value: .float(0), easing: .preset(.easeInOut)), Keyframe(time: 0.5, value: .float(4.99), easing: .preset(.easeInOut)), Keyframe(time: 1, value: .float(0)), ]) ``` ## Reading the Remap Back A track leaves the static value of `playback/timeRemap` unchanged, so `getFloat(_:property:)` still reads 0 for the reversed lane. `isPropertyAnimatable(_:property:)` is true for `playback/timeRemap` on a group and false on other blocks. ```swift highlight-timeRemap-readBack // The track leaves the static remap unchanged let reversedStaticRemap = try engine.block.getFloat(reversed, property: "playback/timeRemap") let remapIsAnimatable = try engine.block.isPropertyAnimatable(reversed, property: "playback/timeRemap") print(reversedStaticRemap, remapIsAnimatable) // 0.0 true ``` ## Handling Errors Only a group has a time remap. On any other block, a call throws an `EngineError` whose `catalogCode` names the cause. ```swift highlight-timeRemap-errors var errorCode: EngineErrorCode? do { // Only a group has a time remap if let ball = try engine.block.getChildren(normal).last { try engine.block.setFloat(ball, property: "playback/timeRemap", value: 1) } } catch let error as EngineError { errorCode = error.catalogCode print(errorCode == .editorBlockNoTimeRemapSupport) // true } ``` ## Exporting a Frame We export the page as PNG at 1.5 seconds. The gray ball is at 30 percent of its lane, the blue ball in the middle, the red ball at 70 percent, and the green ball past the middle on its way out. ```swift highlight-timeRemap-export // Export the frame at 1.5 seconds try engine.block.setPlaybackTime(page, time: 1.5) let pngData = try await engine.block.export(page, mimeType: .png) print(!pngData.isEmpty) // true ``` ## Time Remap and Other Block Operations - Without `group/composition`, the remap has no effect, and the children play on the page clock. - `playback/speed` of a composition group multiplies the remapped time. - A child shows only while the remapped time is at or after its time offset and before its end, so a clock at a child's end, or a reversed or frozen clock, can hide it. - A `.relative` remap track adds its values to the static remap. - Saving and loading, undo and redo keep the remap and its keyframes. ## Troubleshooting - **The block has no time remap** (`EngineErrorCode.editorBlockNoTimeRemapSupport`): set `playback/timeRemap` on a group. - **The block takes no remap keyframes** (`EngineErrorCode.blockKeyframesPropertyNotAnimatable`): set `playback/timeRemap` keyframes on a group. - **The value is not a number** (`EngineErrorCode.blockValueNotFinite`): pass a finite time in seconds. - **The keyframes are refused** (`EngineErrorCode.blockKeyframesValueKindMismatch`): use `.float` keyframes for `playback/timeRemap`. - **The children do not change**: turn on `group/composition` and `playback/timeRemapEnabled` for the group. - **A child disappears**: the remapped time is outside the child's time offset and duration. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.create(_:)` | Create a graphic block | | `engine.block.createShape(_:)` | Create a rect or ellipse shape | | `engine.block.setShape(_:shape:)` | Give a graphic its shape | | `engine.block.createFill(_:)` | Create a color fill | | `engine.block.setColor(_:property:color:)` | Set the fill color | | `engine.block.setFill(_:fill:)` | Give a graphic its fill | | `engine.block.setWidth(_:value:)` | Set the width of a block | | `engine.block.setHeight(_:value:)` | Set the height of a block | | `engine.block.setPositionX(_:value:)` | Set the x position of a block | | `engine.block.setPositionY(_:value:)` | Set the y position of a block | | `engine.block.appendChild(to:child:)` | Add a block to the page | | `engine.block.setDuration(_:duration:)` | Set the duration of a block or group | | `engine.block.group(_:)` | Group blocks into a group | | `engine.block.setBool(_:property:value:)` | Turn on `group/composition` or `playback/timeRemapEnabled` | | `engine.block.getBool(_:property:)` | Check whether the remap is on | | `engine.block.setFloat(_:property:value:)` | Set the static remap, in seconds | | `engine.block.getFloat(_:property:)` | Read the static remap | | `engine.block.setKeyframes(_:property:keyframes:mode:)` | Animate the ball or the remap | | `engine.block.isPropertyAnimatable(_:property:)` | Check that a block takes remap keyframes | | `engine.block.getChildren(_:)` | Get the children of a group | | `engine.block.setPlaybackTime(_:time:)` | Seek before you export a frame | | `engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:)` | Export the frame as PNG | ### Properties | Property | Type | Description | | --- | --- | --- | | `group/composition` | Bool | Give the children of a group their own clock. Default off | | `playback/timeRemapEnabled` | Bool | Use `playback/timeRemap` for the clock of the children. Default off | | `playback/timeRemap` | Float | Time on the children's clock, in seconds. Takes keyframes. Default 0 | ## Next Steps - [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/): easing and the other keyframe value kinds. - [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/): add preset animations to blocks. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Follow Another Block" description: "Make a block follow the position, rotation and scale of another block with a transform parent, also while that block is animated." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/transform-parent-b51eab/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Follow Another Block](https://img.ly/docs/cesdk/mac-catalyst/animation/transform-parent-b51eab/) --- ```swift file=@cesdk_swift_examples/engine-guides-transform-parent/TransformParent.swift reference-only import Foundation import IMGLYEngine struct TransformParentGuideResult { let pivot: DesignBlockID let page: DesignBlockID let planetTransformParent: DesignBlockID? let moonOpacity: Float let moonParent: DesignBlockID? let planetX: Float let planetY: Float let labelTransformParent: DesignBlockID? let cycleErrorCode: EngineErrorCode? let unsupportedErrorCode: EngineErrorCode? let pngData: Data } @MainActor func transformParent(engine: Engine) async throws -> TransformParentGuideResult { let scene = try engine.scene.create() // At 72 DPI one point is one pixel, so the 36 pt label draws at 36 px. try engine.scene.setDesignUnit(.px) try engine.block.setFloat(scene, property: "scene/dpi", value: 72) try engine.block.setFloat(scene, property: "scene/pixelScaleFactor", value: 1) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) try engine.block.setDuration(page, duration: 5) // Creates a circle graphic of the given size and color on the page func createCircle(size: Float, color: Color) throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.ellipse)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: color) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: size) try engine.block.setHeight(block, value: size) try engine.block.appendChild(to: page, child: block) try engine.block.setDuration(block, duration: 5) return block } let pivot = try createCircle(size: 20, color: .rgba(r: 0, g: 0, b: 0, a: 1)) try engine.block.setPositionX(pivot, value: 950) try engine.block.setPositionY(pivot, value: 530) try engine.block.setFloat(pivot, property: "transform/anchor/x", value: 10) try engine.block.setFloat(pivot, property: "transform/anchor/y", value: 10) try engine.block.setKeyframes(pivot, property: "rotation", keyframes: [ Keyframe(time: 0, value: .float(0)), Keyframe(time: 1, value: .float(2 * Float.pi)), ]) try engine.block.setVisible(pivot, visible: false) let planet = try createCircle(size: 120, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) try engine.block.setTransformParent(planet, parent: pivot) let planetTransformParent = try engine.block.getTransformParent(planet) print(planetTransformParent == pivot) // true // The position is now an offset from the pivot's top-left corner, // in the pivot's turning space try engine.block.setPositionX(planet, value: 300) try engine.block.setPositionY(planet, value: -50) // The planet spins twice about its center while it circles the pivot try engine.block.setFloat(planet, property: "transform/anchor/x", value: 60) try engine.block.setFloat(planet, property: "transform/anchor/y", value: 60) try engine.block.setKeyframes(planet, property: "rotation", keyframes: [ Keyframe(time: 0, value: .float(0)), Keyframe(time: 1, value: .float(4 * Float.pi)), ]) // The moon follows the planet, which follows the pivot let moon = try createCircle(size: 40, color: .rgba(r: 0.85, g: 0.85, b: 0.8, a: 1)) try engine.block.setTransformParent(moon, parent: planet) try engine.block.setPositionX(moon, value: 160) try engine.block.setPositionY(moon, value: 40) // A text label follows the planet, too let label = try engine.block.create(.text) try engine.block.replaceText(label, text: "Planet") try engine.block.setTextFontSize(label, fontSize: 36) try engine.block.setWidthMode(label, mode: .auto) try engine.block.setHeightMode(label, mode: .auto) try engine.block.appendChild(to: page, child: label) try engine.block.setTransformParent(label, parent: planet) try engine.block.setPositionX(label, value: 0) try engine.block.setPositionY(label, value: 140) try engine.block.setOpacity(planet, value: 0.5) let moonOpacity = try engine.block.getOpacity(moon) let moonParent = try engine.block.getParent(moon) print(moonOpacity, moonParent == page) // 1.0 true // A quarter of the way through, the pivot has turned by 90 degrees try engine.block.setPlaybackTime(page, time: 1.25) let planetX = try engine.block.getGlobalBoundingBoxX(planet) let planetY = try engine.block.getGlobalBoundingBoxY(planet) print(planetX, planetY) // 900.0 830.0 // The label's position is read in page space again, so place it there try engine.block.setTransformParent(label, parent: nil) let labelTransformParent = try engine.block.getTransformParent(label) print(labelTransformParent == nil) // true try engine.block.setPositionX(label, value: 80) try engine.block.setPositionY(label, value: 80) var cycleErrorCode: EngineErrorCode? do { // The pivot cannot follow a block that follows it try engine.block.setTransformParent(pivot, parent: moon) } catch let error as EngineError { cycleErrorCode = error.catalogCode print(cycleErrorCode == .blockTransformParentCycle) // true } var unsupportedErrorCode: EngineErrorCode? do { // Only graphic and text blocks take part in a link try engine.block.setTransformParent(planet, parent: page) } catch let error as EngineError { unsupportedErrorCode = error.catalogCode print(unsupportedErrorCode == .blockTransformParentUnsupported) // true } // The playback time is still 1.25 seconds, so the frame shows the planet below the pivot let pngData = try await engine.block.export(page, mimeType: .png) try await engine.captureGuide(data: pngData, label: "hero", mimeType: .png) return TransformParentGuideResult( pivot: pivot, page: page, planetTransformParent: planetTransformParent, moonOpacity: moonOpacity, moonParent: moonParent, planetX: planetX, planetY: planetY, labelTransformParent: labelTransformParent, cycleErrorCode: cycleErrorCode, unsupportedErrorCode: unsupportedErrorCode, pngData: pngData, ) } ``` Make a block follow the position, rotation and scale of another block, like a parented layer in a motion design tool. ![A half-transparent blue planet below the page center with its opaque moon, and the unlinked Planet label at the top left, at 1.25 seconds](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-transform-parent) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. A transform parent moves a block without changing the layer list. The block follows the other block wherever it sits on the page, also while that block is animated, and keeps its own opacity, visibility and stacking order. The editor has no control for transform parents, so you set them with the block API. This guide covers how to animate a hidden block that drives others, how to link a block and read the link back, how chains of links work, what a block inherits from its transform parent, how to read the animated position, how to remove a link, the errors to expect, and how to export a frame. ## Animating a Hidden Parent A transform parent can be any graphic or text block. The example draws its blocks as circles with a small local function. ```swift highlight-transformParent-createCircle // Creates a circle graphic of the given size and color on the page func createCircle(size: Float, color: Color) throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.ellipse)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: color) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: size) try engine.block.setHeight(block, value: size) try engine.block.appendChild(to: page, child: block) try engine.block.setDuration(block, duration: 5) return block } ``` We use a small circle at the page center as a pivot. Its anchor, `transform/anchor/x` and `transform/anchor/y`, sits at its center, so its `rotation` keyframes turn it in place, once over 5 seconds. For keyframes and anchors, see [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/). `setVisible(_:visible:)` hides the pivot. A hidden block still works as a transform parent, like a null layer in a motion design tool, so one invisible block can move several others. ```swift highlight-transformParent-hiddenParent let pivot = try createCircle(size: 20, color: .rgba(r: 0, g: 0, b: 0, a: 1)) try engine.block.setPositionX(pivot, value: 950) try engine.block.setPositionY(pivot, value: 530) try engine.block.setFloat(pivot, property: "transform/anchor/x", value: 10) try engine.block.setFloat(pivot, property: "transform/anchor/y", value: 10) try engine.block.setKeyframes(pivot, property: "rotation", keyframes: [ Keyframe(time: 0, value: .float(0)), Keyframe(time: 1, value: .float(2 * Float.pi)), ]) try engine.block.setVisible(pivot, visible: false) ``` ## Linking a Block `setTransformParent(_:parent:)` makes the block follow the transform of the parent. `getTransformParent(_:)` returns the parent. Both blocks must be graphic or text blocks. The block's position, rotation and scale are then read in the local space of the parent. The planet's position of (300, -50) is an offset from the pivot's top-left corner, measured along the pivot's turning axes, so the planet circles the page center while the pivot turns. The block does not keep its place on screen when you link or unlink it, so set its position for the new space after you change the link. ```swift highlight-transformParent-link let planet = try createCircle(size: 120, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) try engine.block.setTransformParent(planet, parent: pivot) let planetTransformParent = try engine.block.getTransformParent(planet) print(planetTransformParent == pivot) // true // The position is now an offset from the pivot's top-left corner, // in the pivot's turning space try engine.block.setPositionX(planet, value: 300) try engine.block.setPositionY(planet, value: -50) ``` ## Building a Chain A block can follow a block that follows a third block. The planet has its own `rotation` track about its center, and the moon follows the planet, so the moon circles the planet while the planet circles the pivot. The text label also follows the planet. Each link adds the transform of its parent, in any order of the blocks on the page. ```swift highlight-transformParent-chain // The planet spins twice about its center while it circles the pivot try engine.block.setFloat(planet, property: "transform/anchor/x", value: 60) try engine.block.setFloat(planet, property: "transform/anchor/y", value: 60) try engine.block.setKeyframes(planet, property: "rotation", keyframes: [ Keyframe(time: 0, value: .float(0)), Keyframe(time: 1, value: .float(4 * Float.pi)), ]) // The moon follows the planet, which follows the pivot let moon = try createCircle(size: 40, color: .rgba(r: 0.85, g: 0.85, b: 0.8, a: 1)) try engine.block.setTransformParent(moon, parent: planet) try engine.block.setPositionX(moon, value: 160) try engine.block.setPositionY(moon, value: 40) // A text label follows the planet, too let label = try engine.block.create(.text) try engine.block.replaceText(label, text: "Planet") try engine.block.setTextFontSize(label, fontSize: 36) try engine.block.setWidthMode(label, mode: .auto) try engine.block.setHeightMode(label, mode: .auto) try engine.block.appendChild(to: page, child: label) try engine.block.setTransformParent(label, parent: planet) try engine.block.setPositionX(label, value: 0) try engine.block.setPositionY(label, value: 140) ``` ## What a Follower Inherits Only the transform is inherited. A follower keeps its own opacity and visibility, its place in the stacking order and its parent in the block hierarchy. The planet is half transparent, while the moon stays opaque, and `getParent(_:)` still returns the page for the moon. ```swift highlight-transformParent-transformOnly try engine.block.setOpacity(planet, value: 0.5) let moonOpacity = try engine.block.getOpacity(moon) let moonParent = try engine.block.getParent(moon) print(moonOpacity, moonParent == page) // 1.0 true ``` ## Reading the Animated Position A follower moves when its transform parent plays keyframes, auto-orients or runs a preset animation. The global bounding box of the follower includes that motion at the current playback time. It uses the static position, rotation and scale of the follower itself, so the follower's own keyframes and animations do not change the box. After `setPlaybackTime(_:time:)` with 1.25 seconds, a quarter of the duration, the pivot has turned by 90 degrees, so `getGlobalBoundingBoxX(_:)` and `getGlobalBoundingBoxY(_:)` place the planet below the page center. ```swift highlight-transformParent-readPosition // A quarter of the way through, the pivot has turned by 90 degrees try engine.block.setPlaybackTime(page, time: 1.25) let planetX = try engine.block.getGlobalBoundingBoxX(planet) let planetY = try engine.block.getGlobalBoundingBoxY(planet) print(planetX, planetY) // 900.0 830.0 ``` ## Removing the Link Pass `nil` as the parent to remove the link. `getTransformParent(_:)` then returns `nil`, and the label's position is read in page space again, so we give it a page position. ```swift highlight-transformParent-unlink // The label's position is read in page space again, so place it there try engine.block.setTransformParent(label, parent: nil) let labelTransformParent = try engine.block.getTransformParent(label) print(labelTransformParent == nil) // true try engine.block.setPositionX(label, value: 80) try engine.block.setPositionY(label, value: 80) ``` ## Handling Errors A failed `setTransformParent(_:parent:)` throws an `EngineError` and adds no link. Its `catalogCode` names the cause. A block cannot follow itself, a block inside it or a block that follows it, directly or through other blocks. A page, a group and every other block type that is not a graphic or text block are refused on either side. ```swift highlight-transformParent-errors var cycleErrorCode: EngineErrorCode? do { // The pivot cannot follow a block that follows it try engine.block.setTransformParent(pivot, parent: moon) } catch let error as EngineError { cycleErrorCode = error.catalogCode print(cycleErrorCode == .blockTransformParentCycle) // true } var unsupportedErrorCode: EngineErrorCode? do { // Only graphic and text blocks take part in a link try engine.block.setTransformParent(planet, parent: page) } catch let error as EngineError { unsupportedErrorCode = error.catalogCode print(unsupportedErrorCode == .blockTransformParentUnsupported) // true } ``` ## Exporting a Frame The playback time is still 1.25 seconds, so we export the page as PNG `Data`. The frame shows the planet below the page center with its moon, and no pivot. ```swift highlight-transformParent-export // The playback time is still 1.25 seconds, so the frame shows the planet below the pivot let pngData = try await engine.block.export(page, mimeType: .png) ``` ## Links and Other Block Operations - Destroying the transform parent removes the link. - `fillParent(_:)` ends the link, because the block then fills its own parent. - Undo and redo, saving and loading, and duplicating keep the link. A copy follows the same parent. In a copy of a group, a block that followed another member follows the copy of that member. - Blocks saved to a string or an archive without their transform parent keep their place on the page and lose the link. Their position, rotation and scale keyframes keep playing in place when the transform parent has no keyframes or animations and no mirror, shear or unequal scale. Otherwise they stay in the space of the transform parent. - A pasted copy does not follow the transform parent again, also when that block is in the scene. - Moving, rotating, scaling or flipping a group or a selection that holds both blocks transforms the follower once. - An auto-orienting block turns in the space of its transform parent. - When the engine enforces scopes, `setTransformParent(_:parent:)` needs the `layer/move` scope. ## Troubleshooting - **A link would make a cycle** (`EngineErrorCode.blockTransformParentCycle`): the parent is the block itself, sits inside it, or already follows it. Link the blocks the other way round, or pick another parent. - **The block type cannot take part in a link** (`EngineErrorCode.blockTransformParentUnsupported`): pass graphic or text blocks on both sides. To move a group, link its children instead. - **The link cannot change** (`EngineErrorCode.blockPositionLocked`): the transform of the block is locked. Unlock it before you link or unlink it. The lock of the parent does not matter. - **A link stops working after a hierarchy change**: moving the transform parent into the follower, for example with `appendChild(to:child:)`, turns the link into a cycle. The engine then ignores the link when it draws the block and reports no error. Remove the link, or move the parent out again. Destroying any block or loading the scene removes such a link. Then move the parent out and link the blocks again. - **The block jumps when you link it**: its position is now read in the space of the parent. Set the position again after the link changes. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.setTransformParent(_:parent:)` | Make a block follow the transform of another block, or pass `nil` to remove the link | | `engine.block.getTransformParent(_:)` | Get the block that a block follows, or `nil` | | `engine.block.create(_:)` | Create the graphic and text blocks | | `engine.block.createShape(_:)` | Create the ellipse shape of a circle | | `engine.block.setShape(_:shape:)` | Give a graphic its shape | | `engine.block.createFill(_:)` | Create a color fill | | `engine.block.setColor(_:property:color:)` | Set the fill color | | `engine.block.setFill(_:fill:)` | Give a graphic its fill | | `engine.block.setWidth(_:value:)` | Set the width of a block | | `engine.block.setHeight(_:value:)` | Set the height of a block | | `engine.block.appendChild(to:child:)` | Add a block to the page | | `engine.block.setDuration(_:duration:)` | Set the duration that keyframe times refer to | | `engine.block.setPositionX(_:value:)` | Set the x position, in the space of the transform parent when a block has one | | `engine.block.setPositionY(_:value:)` | Set the y position, in the space of the transform parent when a block has one | | `engine.block.setFloat(_:property:value:)` | Set the anchor that rotation turns about | | `engine.block.setKeyframes(_:property:keyframes:mode:)` | Animate the rotation of a block | | `engine.block.setVisible(_:visible:)` | Hide the transform parent, which still moves its followers | | `engine.block.replaceText(_:text:in:)` | Set the text of the label | | `engine.block.setTextFontSize(_:fontSize:in:)` | Set the font size of the label | | `engine.block.setWidthMode(_:mode:)` | Size the label to its text | | `engine.block.setHeightMode(_:mode:)` | Size the label to its text | | `engine.block.setOpacity(_:value:)` | Set the opacity, which a follower does not inherit | | `engine.block.getOpacity(_:)` | Read the opacity of a follower | | `engine.block.getParent(_:)` | Get the parent in the block hierarchy, which a link does not change | | `engine.block.setPlaybackTime(_:time:)` | Seek before you read the animated position or export a frame | | `engine.block.getGlobalBoundingBoxX(_:)` | Read the x position of a follower at the current playback time | | `engine.block.getGlobalBoundingBoxY(_:)` | Read the y position of a follower at the current playback time | | `engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:)` | Export the frame as PNG | ## Next Steps - [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/): animate the transform parent with keyframe tracks. - [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/): add preset animations, which a follower also follows. - [Import Lottie Animations](https://img.ly/docs/cesdk/mac-catalyst/import-media/lottie-6bb9ab/): the Lottie import links each layer to its Lottie parent layer with a transform parent. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Supported Animation Types" description: "Apply different animation types to design blocks in CE.SDK and configure their properties." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/animation/types-4e5f41/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) > [Supported Animation Types](https://img.ly/docs/cesdk/mac-catalyst/animation/types-4e5f41/) --- ```swift file=@cesdk_swift_examples/engine-guides-animation-types/AnimationTypes.swift reference-only import Foundation import IMGLYEngine @MainActor func animationTypes(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) let pageFill = try engine.block.createFill(.color) try engine.block.setColor(pageFill, property: "fill/color/value", r: 1, g: 1, b: 1, a: 1) try engine.block.setFill(page, fill: pageFill) let baseURL = try engine.guidesBaseURL let imageURLs = [ baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_3.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_5.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_6.jpg"), ] // 2 columns × 3 rows grid layout for 6 demonstration blocks. let columns = 2 let rows = 3 let blockWidth: Float = 1920 / Float(columns) - 60 let blockHeight: Float = 1080 / Float(rows) - 60 func createImageBlock(index: Int) throws -> DesignBlockID { let graphic = try engine.block.create(.graphic) try engine.block.setShape(graphic, shape: engine.block.createShape(.rect)) let imageFill = try engine.block.createFill(.image) try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: imageURLs[index]) try engine.block.setFill(graphic, fill: imageFill) try engine.block.setWidth(graphic, value: blockWidth) try engine.block.setHeight(graphic, value: blockHeight) let column = index % columns let row = index / columns try engine.block.setPositionX(graphic, value: 30 + Float(column) * (blockWidth + 60)) try engine.block.setPositionY(graphic, value: 30 + Float(row) * (blockHeight + 60)) try engine.block.appendChild(to: page, child: graphic) return graphic } let block1 = try createImageBlock(index: 0) let slideAnimation = try engine.block.createAnimation(.slide) try engine.block.setInAnimation(block1, animation: slideAnimation) try engine.block.setDuration(slideAnimation, duration: 1.0) try engine.block.setFloat(slideAnimation, property: "animation/slide/direction", value: .pi) try engine.block.setEnum(slideAnimation, property: "animationEasing", value: "EaseOut") let block2 = try createImageBlock(index: 1) let fadeAnimation = try engine.block.createAnimation(.fade) try engine.block.setInAnimation(block2, animation: fadeAnimation) try engine.block.setDuration(fadeAnimation, duration: 1.0) try engine.block.setEnum(fadeAnimation, property: "animationEasing", value: "EaseInOut") let block3 = try createImageBlock(index: 2) let zoomAnimation = try engine.block.createAnimation(.zoom) try engine.block.setInAnimation(block3, animation: zoomAnimation) try engine.block.setDuration(zoomAnimation, duration: 1.0) try engine.block.setBool(zoomAnimation, property: "animation/zoom/fade", value: true) let block4 = try createImageBlock(index: 3) let wipeIn = try engine.block.createAnimation(.wipe) try engine.block.setInAnimation(block4, animation: wipeIn) try engine.block.setDuration(wipeIn, duration: 1.0) try engine.block.setEnum(wipeIn, property: "animation/wipe/direction", value: "Right") let fadeOut = try engine.block.createAnimation(.fade) try engine.block.setOutAnimation(block4, animation: fadeOut) try engine.block.setDuration(fadeOut, duration: 1.0) try engine.block.setEnum(fadeOut, property: "animationEasing", value: "EaseIn") let block5 = try createImageBlock(index: 4) let breathingLoop = try engine.block.createAnimation(.breathingLoop) try engine.block.setLoopAnimation(block5, animation: breathingLoop) try engine.block.setDuration(breathingLoop, duration: 2.0) // Intensity: 0 results in a maximum scale of 1.25; 1 results in a maximum scale of 2.5. try engine.block.setFloat(breathingLoop, property: "animation/breathing_loop/intensity", value: 0.3) let block6 = try createImageBlock(index: 5) let spinIn = try engine.block.createAnimation(.spin) try engine.block.setInAnimation(block6, animation: spinIn) try engine.block.setDuration(spinIn, duration: 1.0) try engine.block.setEnum(spinIn, property: "animation/spin/direction", value: "Clockwise") try engine.block.setFloat(spinIn, property: "animation/spin/intensity", value: 0.5) let blurOut = try engine.block.createAnimation(.blur) try engine.block.setOutAnimation(block6, animation: blurOut) try engine.block.setDuration(blurOut, duration: 1.0) let swayLoop = try engine.block.createAnimation(.swayLoop) try engine.block.setLoopAnimation(block6, animation: swayLoop) try engine.block.setDuration(swayLoop, duration: 1.5) let slideProperties = try engine.block.findAllProperties(slideAnimation) let easingOptions = try engine.block.getEnumValues(ofProperty: "animationEasing") // Advance playback so the entrance animations have started by the time the // scene is rendered or exported. try engine.block.setPlaybackTime(page, time: 1.9) _ = slideProperties _ = easingOptions } ``` Apply entrance, exit, and loop animations to design blocks using the available animation types in CE.SDK. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-animation-types) CE.SDK organizes animations into three categories: entrance (In), exit (Out), and loop. Each category determines when the animation plays during the block's lifecycle. This guide demonstrates different animation types and their configurable properties. This guide covers applying entrance animations (slide, fade, zoom), exit animations, loop animations, and configuring animation properties like direction, easing, and intensity. ## Entrance Animations Entrance animations define how a block appears. We use `createAnimation(_:)` with the animation type and attach it using `setInAnimation(_:animation:)`. ### Slide Animation The slide animation moves a block along a direction. The `animation/slide/direction` property is the direction of travel in radians: `0` moves right, `.pi / 2` moves down, `.pi` moves left, and `3 * .pi / 2` moves up. An entrance starts on the opposite side, so `.pi` enters from the right. ```swift highlight-animationTypes-entranceSlide let slideAnimation = try engine.block.createAnimation(.slide) try engine.block.setInAnimation(block1, animation: slideAnimation) try engine.block.setDuration(slideAnimation, duration: 1.0) try engine.block.setFloat(slideAnimation, property: "animation/slide/direction", value: .pi) try engine.block.setEnum(slideAnimation, property: "animationEasing", value: "EaseOut") ``` ### Fade Animation The fade animation transitions opacity from invisible to fully visible. Easing controls the animation curve. ```swift highlight-animationTypes-entranceFade let fadeAnimation = try engine.block.createAnimation(.fade) try engine.block.setInAnimation(block2, animation: fadeAnimation) try engine.block.setDuration(fadeAnimation, duration: 1.0) try engine.block.setEnum(fadeAnimation, property: "animationEasing", value: "EaseInOut") ``` ### Zoom Animation The zoom animation scales the block from a smaller size to its frame size. While it runs, it ignores the block's `transform/scale`. The `animation/zoom/fade` property adds an opacity transition during scaling. ```swift highlight-animationTypes-entranceZoom let zoomAnimation = try engine.block.createAnimation(.zoom) try engine.block.setInAnimation(block3, animation: zoomAnimation) try engine.block.setDuration(zoomAnimation, duration: 1.0) try engine.block.setBool(zoomAnimation, property: "animation/zoom/fade", value: true) ``` Other entrance animation types include: - `.blur` — Transitions from blurred to clear - `.wipe` — Reveals with a directional wipe - `.pop` — Bouncy scale effect - `.spin` — Rotates the block into view - `.grow` — Scales up from a point ### Ken Burns Animation The Ken Burns animation (`.kenBurns`) pans and zooms across an image or video. The pan travels through the space between the block frame and the crop edges. When you attach the animation, the engine sets the content fill mode of the block to `.cover`. When the image or video has loaded, the engine zooms the crop to a scale ratio of 1.3. The content fill mode then becomes `.crop`. When you set the content fill mode to `.cover` again, or reset the crop, the engine zooms the crop again. A scene saved before the zoom gets it when it loads. Attaching the animation replaces a crop that you set before. To use your own crop, set it after the image or video has loaded. ## Exit Animations Exit animations define how a block leaves the screen. We use `setOutAnimation(_:animation:)` to attach them. CE.SDK prevents overlap between entrance and exit durations automatically. ```swift highlight-animationTypes-exitAnimation let wipeIn = try engine.block.createAnimation(.wipe) try engine.block.setInAnimation(block4, animation: wipeIn) try engine.block.setDuration(wipeIn, duration: 1.0) try engine.block.setEnum(wipeIn, property: "animation/wipe/direction", value: "Right") let fadeOut = try engine.block.createAnimation(.fade) try engine.block.setOutAnimation(block4, animation: fadeOut) try engine.block.setDuration(fadeOut, duration: 1.0) try engine.block.setEnum(fadeOut, property: "animationEasing", value: "EaseIn") ``` In this example, a wipe entrance transitions to a fade exit. Mirror entrance effects for visual consistency, or use contrasting effects for emphasis. ## Loop Animations Loop animations run continuously while the block is visible. They can combine with entrance and exit animations. We use `setLoopAnimation(_:animation:)` to attach them. ```swift highlight-animationTypes-loopAnimation let breathingLoop = try engine.block.createAnimation(.breathingLoop) try engine.block.setLoopAnimation(block5, animation: breathingLoop) try engine.block.setDuration(breathingLoop, duration: 2.0) // Intensity: 0 results in a maximum scale of 1.25; 1 results in a maximum scale of 2.5. try engine.block.setFloat(breathingLoop, property: "animation/breathing_loop/intensity", value: 0.3) ``` The duration controls each cycle length. Loop animation types include: - `.breathingLoop` — Slow scale pulse - `.pulsatingLoop` — Rhythmic scale - `.spinLoop` — Continuous rotation - `.fadeLoop` — Opacity cycling - `.swayLoop` — Rotational oscillation - `.jumpLoop` — Jumping motion - `.blurLoop` — Blur cycling - `.squeezeLoop` — Squeezing effect ## Combined Animations A single block can have entrance, exit, and loop animations running together. The loop animation runs throughout the block's visibility while entrance and exit animations play at the appropriate times. ```swift highlight-animationTypes-combinedAnimations let spinIn = try engine.block.createAnimation(.spin) try engine.block.setInAnimation(block6, animation: spinIn) try engine.block.setDuration(spinIn, duration: 1.0) try engine.block.setEnum(spinIn, property: "animation/spin/direction", value: "Clockwise") try engine.block.setFloat(spinIn, property: "animation/spin/intensity", value: 0.5) let blurOut = try engine.block.createAnimation(.blur) try engine.block.setOutAnimation(block6, animation: blurOut) try engine.block.setDuration(blurOut, duration: 1.0) let swayLoop = try engine.block.createAnimation(.swayLoop) try engine.block.setLoopAnimation(block6, animation: swayLoop) try engine.block.setDuration(swayLoop, duration: 1.5) ``` ## Configuring Animation Properties Each animation type has specific configurable properties. We use `findAllProperties(_:)` to discover available properties and `getEnumValues(ofProperty:)` to query options for enum properties. ```swift highlight-animationTypes-discoverProperties let slideProperties = try engine.block.findAllProperties(slideAnimation) let easingOptions = try engine.block.getEnumValues(ofProperty: "animationEasing") ``` Common configurable properties include: - **Direction**: Controls entry/exit direction in radians or enum values - **Easing**: Animation curve (`Linear`, `EaseIn`, `EaseOut`, `EaseInOut`) - **Intensity**: Strength of the effect (varies by animation type) - **Fade**: Whether to include opacity transition ## API Reference | Method | Description | | --- | --- | | `engine.block.createAnimation(_:)` | Create animation by type | | `engine.block.setInAnimation(_:animation:)` | Attach entrance animation | | `engine.block.setOutAnimation(_:animation:)` | Attach exit animation | | `engine.block.setLoopAnimation(_:animation:)` | Attach loop animation | | `engine.block.setDuration(_:duration:)` | Set animation duration | | `engine.block.setFloat(_:property:value:)` | Set numeric property | | `engine.block.setEnum(_:property:value:)` | Set enum property | | `engine.block.setBool(_:property:value:)` | Set boolean property | | `engine.block.findAllProperties(_:)` | Discover configurable properties | | `engine.block.getEnumValues(ofProperty:)` | Get available enum values | ## Next Steps - [Base Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/base-0fc5c4/) — Create and attach animations to blocks - [Text Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create/text-d6f4aa/) — Animate text with writing styles - [Animation Overview](https://img.ly/docs/cesdk/mac-catalyst/animation/overview-6a2ef2/) — Animation concepts and capabilities --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "API Reference" description: "Find out how to use the API of the CESDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/api-reference/overview-8f24e1/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [API Reference](https://img.ly/docs/cesdk/mac-catalyst/api-reference/overview-8f24e1/) --- For Mac Catalyst, the following packages are available: - [IMGLYEngine](https://img.ly/docs/cesdk/mac-catalyst/api-reference/documentation/imglyengine/): The core engine package for rendering and creative operations. All other packages are built on top of it. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Automate Workflows" description: "Automate repetitive editing tasks using CE.SDK’s headless APIs to generate assets at scale." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/automation-715209/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/mac-catalyst/automation-715209/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/automation/overview-34d971/) - Automate repetitive editing tasks using CE.SDK’s headless APIs to generate assets at scale. - [Batch Processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/) - Documentation for Batch Processing - [Auto-Resize](https://img.ly/docs/cesdk/mac-catalyst/automation/auto-resize-4c2d58/) - Configure blocks to dynamically adjust dimensions using Absolute, Percent, and Auto sizing modes for responsive layouts and content-driven expansion. - [Data Merge](https://img.ly/docs/cesdk/mac-catalyst/automation/data-merge-ae087c/) - Automatically generate personalized designs from a template by merging external data into its variables and placeholder blocks. - [Product Variations](https://img.ly/docs/cesdk/mac-catalyst/automation/product-variations-f3349f/) - Generate multiple product variants from a single template by swapping text, images and styles programmatically. - [Automate Design Generation](https://img.ly/docs/cesdk/mac-catalyst/automation/design-generation-98a99e/) - Populate a reusable template from application data and export the finished design with CE.SDK. - [Multiple Image Generation](https://img.ly/docs/cesdk/mac-catalyst/automation/multi-image-generation-2a0de4/) - Create many image variants from structured data by interpolating content into reusable design templates. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Auto-Resize" description: "Configure blocks to dynamically adjust dimensions using Absolute, Percent, and Auto sizing modes for responsive layouts and content-driven expansion." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/automation/auto-resize-4c2d58/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/mac-catalyst/automation-715209/) > [Auto-Resize](https://img.ly/docs/cesdk/mac-catalyst/automation/auto-resize-4c2d58/) --- ```swift file=@cesdk_swift_examples/engine-guides-auto-resize/AutoResize.swift reference-only import IMGLYEngine @MainActor func autoResize(engine: Engine) async throws { let scene = try engine.scene.create(designUnit: .px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let titleBlock = try engine.block.create(.text) try engine.block.replaceText(titleBlock, text: "Auto-Resize Demo") try engine.block.setTextFontSize(titleBlock, fontSize: 64) try engine.block.setWidthMode(titleBlock, mode: .auto) try engine.block.setHeightMode(titleBlock, mode: .auto) try engine.block.appendChild(to: page, child: titleBlock) let coverBlock = try engine.block.create(.graphic) try engine.block.setShape(coverBlock, shape: engine.block.createShape(.rect)) let coverFill = try engine.block.createFill(.color) try engine.block.setColor(coverFill, property: "fill/color/value", color: .rgba(r: 1, g: 1, b: 1, a: 0.08)) try engine.block.setFill(coverBlock, fill: coverFill) try engine.block.appendChild(to: page, child: coverBlock) try engine.block.fillParent(coverBlock) try engine.block.destroy(coverBlock) await Task.yield() let titleWidth = try engine.block.getFrameWidth(titleBlock) let titleHeight = try engine.block.getFrameHeight(titleBlock) print("Title dimensions: \(Int(titleWidth))x\(Int(titleHeight)) pixels") let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) let centerX = (pageWidth - titleWidth) / 2 let centerY = (pageHeight - titleHeight) / 2 - 100 try engine.block.setPositionX(titleBlock, value: centerX) try engine.block.setPositionY(titleBlock, value: centerY) let backgroundBlock = try engine.block.create(.graphic) try engine.block.setShape(backgroundBlock, shape: engine.block.createShape(.rect)) let backgroundFill = try engine.block.createFill(.color) try engine.block.setColor(backgroundFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.8, a: 0.3)) try engine.block.setFill(backgroundBlock, fill: backgroundFill) try engine.block.setWidthMode(backgroundBlock, mode: .percent) try engine.block.setHeightMode(backgroundBlock, mode: .percent) try engine.block.setWidth(backgroundBlock, value: 0.8) try engine.block.setHeight(backgroundBlock, value: 0.3) try engine.block.setPositionX(backgroundBlock, value: pageWidth * 0.1) try engine.block.setPositionY(backgroundBlock, value: pageHeight * 0.6) try engine.block.appendChild(to: page, child: backgroundBlock) try engine.block.sendToBack(backgroundBlock) let subtitleBlock = try engine.block.create(.text) try engine.block.replaceText(subtitleBlock, text: "Text automatically sizes to fit content") try engine.block.setTextFontSize(subtitleBlock, fontSize: 32) try engine.block.setWidthMode(subtitleBlock, mode: .auto) try engine.block.setHeightMode(subtitleBlock, mode: .auto) try engine.block.appendChild(to: page, child: subtitleBlock) await Task.yield() let subtitleWidth = try engine.block.getFrameWidth(subtitleBlock) try engine.block.setPositionX(subtitleBlock, value: (pageWidth - subtitleWidth) / 2) try engine.block.setPositionY(subtitleBlock, value: pageHeight * 0.7) let titleWidthMode = try engine.block.getWidthMode(titleBlock) let titleHeightMode = try engine.block.getHeightMode(titleBlock) let backgroundWidthMode = try engine.block.getWidthMode(backgroundBlock) let backgroundHeightMode = try engine.block.getHeightMode(backgroundBlock) print("Title uses auto sizing: \(titleWidthMode == .auto && titleHeightMode == .auto)") print("Background uses percent sizing: \(backgroundWidthMode == .percent && backgroundHeightMode == .percent)") // Most-evolved scene — promoted to the guide's hero image. try await engine.captureGuide(page, label: "hero") } ``` Configure blocks to size themselves from fixed values, their parent, or their content. Use the Swift Engine's `.absolute`, `.percent`, and `.auto` size modes on each axis, then read `getFrameWidth(_:)` and `getFrameHeight(_:)` after layout when you need the computed result. ![Auto-Resize layout with an auto-sized title, a percent-sized panel, and a centered auto-sized subtitle](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-auto-resize) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. This example uses a title with Auto mode, a background panel with Percent mode, and computed frame sizes to center text. The Swift Engine also exposes `fillParent(_:)` as a shortcut when an attached block should cover its parent in one call. ## Initialize the engine Create a design scene with an 800 by 600 page so the percent-mode values have a predictable parent size. ```swift highlight-autoResize-setup let scene = try engine.scene.create(designUnit: .px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) ``` ## Size modes - `.absolute` is the default. Width and height are design units that you control directly with `setWidth(_:value:)` and `setHeight(_:value:)`. - `.percent` interprets width and height as normalized parent-relative values. `1.0` means 100 percent of the parent on that axis. - `.auto` lets the engine compute the block size from its content. This is most useful for text and other intrinsic-content blocks. ## Auto mode for text Use Auto mode when content should decide the final frame. Here the title expands to fit its text instead of using a hard-coded width. ```swift highlight-autoResize-autoMode let titleBlock = try engine.block.create(.text) try engine.block.replaceText(titleBlock, text: "Auto-Resize Demo") try engine.block.setTextFontSize(titleBlock, fontSize: 64) try engine.block.setWidthMode(titleBlock, mode: .auto) try engine.block.setHeightMode(titleBlock, mode: .auto) try engine.block.appendChild(to: page, child: titleBlock) ``` ## Fill the parent in one call Attach a block to a parent, then call `fillParent(_:)` to resize and position it so it covers the parent completely: ```swift highlight-autoResize-fillParent let coverBlock = try engine.block.create(.graphic) try engine.block.setShape(coverBlock, shape: engine.block.createShape(.rect)) let coverFill = try engine.block.createFill(.color) try engine.block.setColor(coverFill, property: "fill/color/value", color: .rgba(r: 1, g: 1, b: 1, a: 0.08)) try engine.block.setFill(coverBlock, fill: coverFill) try engine.block.appendChild(to: page, child: coverBlock) try engine.block.fillParent(coverBlock) ``` `fillParent(_:)` also resets crop values when needed and can switch a crop-based fill to cover so the block stays in a valid state. ## Read computed frame dimensions Layout values are not the same as the raw width and height properties in Auto mode. Read frame dimensions after the engine has performed a layout update. ```swift highlight-autoResize-readFrameDimensions let titleWidth = try engine.block.getFrameWidth(titleBlock) let titleHeight = try engine.block.getFrameHeight(titleBlock) print("Title dimensions: \(Int(titleWidth))x\(Int(titleHeight)) pixels") ``` If you query the frame size immediately after changing content, yield to the next turn (`await Task.yield()`) or wait for another engine update before reading. ## Center the block with frame dimensions Once you have the computed title size, use the page dimensions to place it precisely. ```swift highlight-autoResize-centerBlock let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) let centerX = (pageWidth - titleWidth) / 2 let centerY = (pageHeight - titleHeight) / 2 - 100 try engine.block.setPositionX(titleBlock, value: centerX) try engine.block.setPositionY(titleBlock, value: centerY) ``` This pattern is useful whenever content length changes between generated outputs. ## Percent mode for responsive layouts Percent mode makes a block track its parent. The example uses 80 percent width, 30 percent height, with a 10 percent left margin and a 60 percent top offset. ```swift highlight-autoResize-percentMode let backgroundBlock = try engine.block.create(.graphic) try engine.block.setShape(backgroundBlock, shape: engine.block.createShape(.rect)) let backgroundFill = try engine.block.createFill(.color) try engine.block.setColor(backgroundFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.8, a: 0.3)) try engine.block.setFill(backgroundBlock, fill: backgroundFill) try engine.block.setWidthMode(backgroundBlock, mode: .percent) try engine.block.setHeightMode(backgroundBlock, mode: .percent) try engine.block.setWidth(backgroundBlock, value: 0.8) try engine.block.setHeight(backgroundBlock, value: 0.3) try engine.block.setPositionX(backgroundBlock, value: pageWidth * 0.1) try engine.block.setPositionY(backgroundBlock, value: pageHeight * 0.6) try engine.block.appendChild(to: page, child: backgroundBlock) try engine.block.sendToBack(backgroundBlock) ``` Because values are normalized, the same layout logic adapts to different page sizes without recalculating pixel dimensions. ## Additional auto-sized content You can repeat the same pattern for other text blocks. This subtitle uses Auto mode and recenters itself from its computed width. ```swift highlight-autoResize-subtitleAuto let subtitleBlock = try engine.block.create(.text) try engine.block.replaceText(subtitleBlock, text: "Text automatically sizes to fit content") try engine.block.setTextFontSize(subtitleBlock, fontSize: 32) try engine.block.setWidthMode(subtitleBlock, mode: .auto) try engine.block.setHeightMode(subtitleBlock, mode: .auto) try engine.block.appendChild(to: page, child: subtitleBlock) await Task.yield() let subtitleWidth = try engine.block.getFrameWidth(subtitleBlock) try engine.block.setPositionX(subtitleBlock, value: (pageWidth - subtitleWidth) / 2) try engine.block.setPositionY(subtitleBlock, value: pageHeight * 0.7) ``` ## Verify the active modes Query the current modes when you need to branch behavior or assert that template setup is correct. ```swift highlight-autoResize-checkModes let titleWidthMode = try engine.block.getWidthMode(titleBlock) let titleHeightMode = try engine.block.getHeightMode(titleBlock) let backgroundWidthMode = try engine.block.getWidthMode(backgroundBlock) let backgroundHeightMode = try engine.block.getHeightMode(backgroundBlock) print("Title uses auto sizing: \(titleWidthMode == .auto && titleHeightMode == .auto)") print("Background uses percent sizing: \(backgroundWidthMode == .percent && backgroundHeightMode == .percent)") ``` ## Troubleshooting **Frame dimensions are `0` or stale**: wait for a layout pass before calling `getFrameWidth(_:)` or `getFrameHeight(_:)`. **Percent sizing has no effect**: the block must be attached to a parent, and the parent needs a resolved size. **Auto sizing does not change the block**: Auto mode is primarily useful for blocks with intrinsic content, such as text. **A fill looks different after `fillParent(_:)`**: the helper may reset crop values or force cover mode to keep the block valid. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.getWidth(_:)` | Read the configured width value in the current mode | | `engine.block.setWidth(_:value:)` | Set width in the current mode | | `engine.block.getWidthMode(_:)` | Read the width sizing mode | | `engine.block.setWidthMode(_:mode:)` | Set the width sizing mode | | `engine.block.getHeight(_:)` | Read the configured height value in the current mode | | `engine.block.setHeight(_:value:)` | Set height in the current mode | | `engine.block.getHeightMode(_:)` | Read the height sizing mode | | `engine.block.setHeightMode(_:mode:)` | Set the height sizing mode | | `engine.block.getFrameWidth(_:)` | Read the computed width after layout | | `engine.block.getFrameHeight(_:)` | Read the computed height after layout | | `engine.block.fillParent(_:)` | Resize and reposition a block to cover its parent | ## Next Steps - [Resize](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/resize-407242/) — change a block frame explicitly with width and height values. - [Batch Processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/) — apply the same sizing logic across many records. - [Multiple Image Generation](https://img.ly/docs/cesdk/mac-catalyst/automation/multi-image-generation-2a0de4/) — combine template data replacement with responsive layout rules. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Batch Processing" description: "Documentation for Batch Processing" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/mac-catalyst/automation-715209/) > [Batch Processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/) --- Batch processing lets your app automatically generate scores of assets from a single design template. For example, you might create 100 personalized posters or social posts from a CSV file of names and photos, without opening the editor for each one. CE.SDK’s headless engine makes this possible entirely in Swift. This guide shows you how to do that in Swift for iOS, macOS, and Catalyst. You’ll learn how to load a saved design, substitute text and images, and export each variation as an asset file. The same techniques apply to more complex outputs like PDFs or videos. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-automation-batch) ## What You’ll Learn - How to start CE.SDK’s **headless engine** without a UI editor. - How to **load a template** from an archive and attach it to a new scene. - How to **replace variables and images** for each record in your data. - How to **export** each generated design as a common format like PNG, JPEG or PDF. ## When You’ll Use This Headless batch generation is ideal for tasks that need automation, not user interaction. Use it to mass-produce: - Branded materials - Social media graphics - Dynamic thumbnails Because you're not displaying the editor UI, it works equally well on iOS, macOS, and Catalyst. ## Headless Engine At the center of CE.SDK is the `Engine`, a lightweight rendering system you can use without the prebuilt editors. It can run in the background, respond to async tasks, and render scenes directly to image data. ```swift let engine = try await Engine(license: "") ``` For automation, you’ll typically create one `Engine` instance for the full batch run. - **On mobile**, a single-engine, sequential approach is safest. - **On more powerful hardware**, you can explore modest parallelism, as each instance of `Engine` is independent. ## Loading Templates The template defines the design you’ll use for all generated images. You can: 1. Create a template in the CE.SDK editor. 2. Save it as an archive. 3. Add that archive to your app bundle under **Copy Bundle Resources** in Xcode. Or host it somewhere with a valid `URL` for the batch to use. ```swift static var archiveURL: URL { guard let url = Bundle.main.url(forResource: "Template", withExtension: "archive") else { fatalError("Missing Template.archive in bundle") } return url } ``` **Archives** are self-contained, they include: - Your layout - Text - All linked assets. They’re ideal for predictable batch exports. You can choose to save templates as `String` types, but in those cases, the `URL` of every asset must resolve correctly at runtime. Once loaded, always validate the structure before using it. ```swift let blocks = try await engine.block.loadArchive(from: url) if blocks.isEmpty { throw BatchError.invalidTemplate } ``` This ensures that missing or corrupt templates don’t interrupt your batch. `loadArchive(from:)` returns the blocks for your design, which you then attach to a page so the engine can render, modify, and export it. If you want the archive to replace the whole scene, load it with `engine.scene.load(from:)` instead. ```swift let scene = try await engine.scene.load(from: url) ``` ## Supplying Data from JSON Every batch needs a list of records. Each record holds the values to apply to the template. A common pattern is: 1. Store them as a JSON array. 2. Decode them during the batch. A record might have these properties. ```swift struct Record: Codable, Hashable { var id: String var variables: [String: String] var outputFileName: String var images: [String: String]? // optional blockName → bundled image name } ``` Then decode any JSON using a standard pattern. ```swift func loadRecords() -> [Record] { guard let url = Bundle.main.url(forResource: "records", withExtension: "json"), let data = try? Data(contentsOf: url) else { return [] } return (try? JSONDecoder().decode([Record].self, from: data)) ?? [] } ``` Example `records.json` ```json [ { "id": "001", "variables": { "name": "Ruth", "tagline": "Ship great apps" }, "outputFileName": "badge-ruth" }, { "id": "002", "variables": { "name": "Chris", "tagline": "Move fast, polish later" }, "outputFileName": "badge-chris" } ] ``` In a production environment, you’ll load data from an API or database instead of the bundle. If your dataset is large, consider streaming it in chunks instead of loading everything at once. ## Templates and Variables Templates often include placeholders, or variables, that you can update with real data at runtime. In CE.SDK (Swift), template variables follow a key/value pattern and are **always stored as strings**. Your app can convert them into types like numbers or colors when needed. For text blocks, CE.SDK automatically matches placeholders in the template with variable names. Displaying `\{\{username\}\}` as the text in a text box, becomes the variable `username` you can replace with a person’s name before exporting. ```swift // All variables are set via (key:String, value:String) try engine.variable.set(key: "name", value: "Chris") // text try engine.variable.set(key: "price", value: "9.99") // number encoded as string try engine.variable.set(key: "brandColor", value: "#FFD60A") // color as hex string try engine.variable.set(key: "isFeatured", value: "true") // boolean as "true" / "false" try engine.variable.set(key: "imageURL", value: record.imageURL.absoluteString) // URL as string ``` Discover the available variable keys at runtime to validate a template using: ```swift let keys = engine.variable.findAll() // assert or log missing keys before a long batch run ``` ## Applying Data to the Template Once the engine loads the template, you can fill in variables. These correspond to the placeholders you set in your CE.SDK scene, like `\{\{name\}\}` or `\{\{tagline\}\}`. ```swift @MainActor func applyVariables(_ engine: Engine, values: [String: String]) throws { for (key, value) in values { try engine.variable.set(key: key, value: value) } } ``` You can also swap out placeholder images at runtime. The simplest method is to find the block by its name and update its image fill. ```swift let matches = try engine.block.find(byName: "productImage") if let imageBlock = matches.first { let fill = try engine.block.getFill(block) try engine.block.setString(fill, property: "fill/image/fileURI", value: record.imageURL.absoluteString) try engine.block.setFill(imageBlock, fill: fill) try engine.block.setKind(imageBlock, kind: "image") } ``` This snippet looks up a block named `productImage` and replaces its image fill with the URL of the new image. > **Note:** Using block names keeps your automation readable and less fragile than referencing IDs. ## Create Thumbnails You can generate previews by exporting a scaled version of each result: ```swift func exportThumbnail(from engine: Engine, fileName: String, scale: CGFloat = 0.25) throws -> URL { let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let thumbURL = dir.appendingPathComponent("thumb_\(fileName).jpg") let root = try engine.scene.get() let width = try engine.block.getFrameWidth(root) * Float(scale) let height = try engine.block.getFrameHeight(root) * Float(scale) let options = ExportOptions(jpegQuality: 0.7, targetWidth: width, targetHeight: height) let exportData = try engine.block.export(root, mimeType: .jpeg, options: options) try exportData.write(to:url) return thumbURL } ``` ## Exporting to Multiple Formats Exports can target different output types. Just switch the mime type you pass: ```swift let pngData = try await engine.block.export(page, mimeType: .png, options: ExportOptions(targetHeight: 1080)) let pdfData = try await engine.block.export(page, mimeType: .pdf) ``` |Format| MimeType| Typical Use| |---|---|---| |PNG |image/png |Lossless images with transparency| |JPEG |image/jpeg |Photos and smaller files| |PDF |application/pdf |Printable designs| |MP4 |video/mp4 |Animated or timed templates| Use an `ExportOptions` struct to tune output quality, size and other properties of the export. You can get the details in the [Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) guides. If you need multiple formats at once, run several export calls back-to-back using the same engine and page. ## Managing Memory and Resources Each export involves GPU textures, image buffers, and temporary files. To keep your app responsive: - Reuse a single engine for sequential jobs. - Clean up temporary directories between batches. ## Performance Tuning Checklist - Use JPEG quality 0.8–0.9 to balance file size and speed. - Keep templates plain. Avoid unnecessary effects or large images. - Chunk data into smaller groups for large datasets. - Limit concurrency to 2–3 parallel tasks. - Profile on the lowest device you support. ## Error Handling and Retries Batch jobs can fail for network hiccups or invalid data. Use Swift’s do/catch blocks to retry a few times before giving up. ```swift for record in records { var attempts = 0 while attempts < 3 { do { try await exportRecord(record) break } catch { attempts += 1 try await Task.sleep(nanoseconds: UInt64(Double(attempts) * 0.5e9)) } } } ``` You can also log each attempt for easier debugging. ## Logging and Monitoring Progress Adding logging helps track how long each export takes: ```swift import os.log let logger = Logger(subsystem: "com.example.batch", category: "automation") logger.info("Exported \(record.name, privacy: .public)") ``` Wrap your entire run in timestamps using standard Swift `Date` or `DispatchTime` to measure throughput and display progress in your SwiftUI interface. ## Batch Workflow Batch processing isn’t limited to mobile apps. The same logic can run on backends or web services using CE.SDK for Web or Node. If your workload scales beyond device limits, consider: 1. Migrating automation to a server workflow. 2. Sending results back to the app. An example batch process, below, calls `processRecord(_:)` for each record in the data set. The record is processed by: 1. loading the template 2. setting variables 3. replacing images 4. exporting the result ```swift @MainActor func processRecord(_ record: Record) async throws -> URL { let engine = try await EngineFactory.make() let scene = try await engine.scene.load(from: Template.archiveURL) try applyVariables(engine, values: record.variables) if let imgs = record.images { for (blockName, fileName) in imgs { try replaceNamedImage(engine, name: blockName, fileName: fileName) } } let outURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("\(record.outputFileName).jpg") try Exporter.exportJPEG(engine, sceneBlock: scene, to: outURL, quality: 0.9) return outURL } struct EngineFactory { static func make() async throws -> Engine { let engine = try await Engine(license: secrets.licenseKey) return engine } } func replaceNamedImage(_ engine: Engine, name: String, fileName: String) throws { guard let fileURL = Bundle.main.url(forResource: fileName, withExtension: nil) else { return } if let block = try engine.block.find(byName: name).first { // Update the block's image fill via its fileURI let fill = try engine.block.getFill(block) try engine.block.setString(fill, property: "fill/image/fileURI", value: fileURL.absoluteString) try engine.block.setFill(block, fill: fill) } } enum Exporter { @MainActor static func exportJPEG(_ engine: Engine, sceneOrPage: DesignBlockID, to url: URL, quality: Float = 0.9) async throws { let options = ExportOptions(jpegQuality: quality) let exportedData = try await engine.block.export(sceneOrPage, mimeType: .jpeg, options: options) try exportedData.write(to: url) } } ``` Use a small concurrency limit for parallel runs: ```swift @MainActor func runBatchParallel(records: [Record], maxConcurrent: Int = 3) async { await withTaskGroup(of: Void.self) { group in var iterator = records.makeIterator() for _ in 0.. --- title: "Data Merge" description: "Automatically generate personalized designs from a template by merging external data into its variables and placeholder blocks." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/automation/data-merge-ae087c/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/mac-catalyst/automation-715209/) > [Data Merge](https://img.ly/docs/cesdk/mac-catalyst/automation/data-merge-ae087c/) --- ```swift file=@cesdk_swift_examples/engine-guides-data-merge/DataMerge.swift reference-only import Foundation import IMGLYEngine @MainActor func dataMerge(engine: Engine) async throws { // Resolve sample assets against the engine's configured base URL. let baseURL = try engine.guidesBaseURL // Sample record whose fields map to the template's variables and placeholders let record: [String: String] = [ "name": "Alex Smith", "title": "Creative Developer", "email": "alex.smith@example.com", ] let photoURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") // Demo setup: build a minimal template inline. In production, load a // template scene authored on the web with `engine.scene.load(from:)`. let scene = try engine.scene.create() try engine.scene.setDesignUnit(.px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 400) try engine.block.appendChild(to: scene, child: page) // A photo placeholder with a semantic name let photoBlock = try engine.block.create(.graphic) try engine.block.setShape(photoBlock, shape: engine.block.createShape(.rect)) let photoFill = try engine.block.createFill(.image) try engine.block.setURL(photoFill, property: "fill/image/imageFileURI", value: photoURL) try engine.block.setFill(photoBlock, fill: photoFill) try engine.block.setWidth(photoBlock, value: 150) try engine.block.setHeight(photoBlock, value: 150) try engine.block.setPositionX(photoBlock, value: 50) try engine.block.setPositionY(photoBlock, value: 125) try engine.block.setName(photoBlock, name: "profile-photo") try engine.block.appendChild(to: page, child: photoBlock) // A text block referencing variable tokens let textBlock = try engine.block.create(.text) try engine.block.replaceText(textBlock, text: "{{name}}\n{{title}}\n{{email}}") try engine.block.setWidthMode(textBlock, mode: .auto) try engine.block.setHeightMode(textBlock, mode: .auto) try engine.block.setFloat(textBlock, property: "text/fontSize", value: 32) try engine.block.setPositionX(textBlock, value: 230) try engine.block.setPositionY(textBlock, value: 140) try engine.block.appendChild(to: page, child: textBlock) // Discover which variables the loaded template expects let variableNames = engine.variable.findAll() print("Template variables:", variableNames) // Confirm the text block actually references variables let hasVariables = try engine.block.referencesAnyVariables(textBlock) print("Text block references variables:", hasVariables) // Populate each variable with a value from the record for (key, value) in record { try engine.variable.set(key: key, value: value) } // Find a placeholder block by its semantic name and swap its image content if let foundPhotoBlock = engine.block.find(byName: "profile-photo").first { let fill = try engine.block.getFill(foundPhotoBlock) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) } // Export the personalized design as PNG data let blob = try await engine.block.export(page, mimeType: .png) print("Exported PNG data:", blob.count, "bytes") } ``` Automatically generate personalized designs from a template by merging external data into its variables and placeholder blocks — no editor UI required. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-data-merge) Data merge populates a template with external data using the headless Creative Engine, producing personalized outputs like certificates, badges, or team cards without opening the editor. The template itself is authored on the web; on mobile you consume it and merge in per-record data. This guide walks through the core merge operations: discovering variables, setting values, updating placeholder blocks, and exporting the result. ## Prepare the Data Record In production the record comes from a CSV file, database, or API response. For this guide we define a small dictionary whose keys match the template's variables. ```swift highlight-sample-data // Sample record whose fields map to the template's variables and placeholders let record: [String: String] = [ "name": "Alex Smith", "title": "Creative Developer", "email": "alex.smith@example.com", ] let photoURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") ``` Each key maps to a `{{variableName}}` token in the template's text blocks. ## Set Up the Demo Template Templates are designed on the web and loaded with `engine.scene.load(from:)` on mobile. For this self-contained example we build the same structure inline — a photo placeholder plus a text block with variable tokens — so the guide runs standalone. ```swift highlight-setup-template // Demo setup: build a minimal template inline. In production, load a // template scene authored on the web with `engine.scene.load(from:)`. let scene = try engine.scene.create() try engine.scene.setDesignUnit(.px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 400) try engine.block.appendChild(to: scene, child: page) // A photo placeholder with a semantic name let photoBlock = try engine.block.create(.graphic) try engine.block.setShape(photoBlock, shape: engine.block.createShape(.rect)) let photoFill = try engine.block.createFill(.image) try engine.block.setURL(photoFill, property: "fill/image/imageFileURI", value: photoURL) try engine.block.setFill(photoBlock, fill: photoFill) try engine.block.setWidth(photoBlock, value: 150) try engine.block.setHeight(photoBlock, value: 150) try engine.block.setPositionX(photoBlock, value: 50) try engine.block.setPositionY(photoBlock, value: 125) try engine.block.setName(photoBlock, name: "profile-photo") try engine.block.appendChild(to: page, child: photoBlock) // A text block referencing variable tokens let textBlock = try engine.block.create(.text) try engine.block.replaceText(textBlock, text: "{{name}}\n{{title}}\n{{email}}") try engine.block.setWidthMode(textBlock, mode: .auto) try engine.block.setHeightMode(textBlock, mode: .auto) try engine.block.setFloat(textBlock, property: "text/fontSize", value: 32) try engine.block.setPositionX(textBlock, value: 230) try engine.block.setPositionY(textBlock, value: 140) try engine.block.appendChild(to: page, child: textBlock) ``` In a real application, replace this block with a single `engine.scene.load(from: templateURL)` call pointing at a template your team already authored. ## Discover Variables Inspect what the template expects with `engine.variable.findAll()`. This is useful when processing templates you did not author or when validating incoming data. `engine.block.referencesAnyVariables(_:)` confirms whether a specific block depends on variable tokens. ```swift highlight-discover-variables // Discover which variables the loaded template expects let variableNames = engine.variable.findAll() print("Template variables:", variableNames) // Confirm the text block actually references variables let hasVariables = try engine.block.referencesAnyVariables(textBlock) print("Text block references variables:", hasVariables) ``` ## Set Variable Values Apply the record to the template with `engine.variable.set(key:value:)`. Every text block referencing a variable updates immediately. ```swift highlight-set-variables // Populate each variable with a value from the record for (key, value) in record { try engine.variable.set(key: key, value: value) } ``` Variables belong to the scene and stay until you load another scene, so you can set them once and export as many variations as you need. ## Update a Placeholder Block Locate a placeholder block by its semantic name with `engine.block.find(byName:)`, then swap its fill's image URI to replace the content. The pattern works for profile photos, logos, product images, or any image placeholder the template author named. ```swift highlight-update-placeholder // Find a placeholder block by its semantic name and swap its image content if let foundPhotoBlock = engine.block.find(byName: "profile-photo").first { let fill = try engine.block.getFill(foundPhotoBlock) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) } ``` If you need to discover every placeholder without knowing names in advance, use `engine.block.findAllPlaceholders()` instead. ## Export the Design Render the personalized design with `engine.block.export(_:mimeType:)`. The engine returns the encoded bytes; write them to disk, upload them, or pass them to another process. ```swift highlight-export // Export the personalized design as PNG data let blob = try await engine.block.export(page, mimeType: .png) print("Exported PNG data:", blob.count, "bytes") ``` PNG, JPEG, WebP, and PDF are all supported. To process many records in a row, reset variable values between iterations or load a fresh copy of the template for each one. ## Troubleshooting ### Variables Not Rendering If variable placeholders show instead of values: - Verify the variable name matches the template exactly — names are case-sensitive - Call `engine.variable.findAll()` to confirm the variable exists in the loaded scene - Ensure `engine.variable.set(key:value:)` was called before exporting ### Placeholder Not Found If `find(byName:)` returns an empty array: - Confirm the block name was set in the template with `engine.block.setName(_:name:)` - Check the name string matches exactly — names are case-sensitive - Verify the block exists in the current scene ### Image Not Updating If a placeholder image does not change: - Get the fill block first with `engine.block.getFill(_:)` - Use the `fill/image/imageFileURI` property path - Verify the image URL is reachable from the device ## API Reference | Method | Description | |--------|-------------| | `engine.scene.load(from:)` | Load a template scene from a URL | | `engine.variable.findAll()` | List every variable name defined in the scene | | `engine.variable.set(key:value:)` | Set a variable's value | | `engine.variable.get(key:)` | Read the current value of a variable | | `engine.block.referencesAnyVariables(_:)` | Check whether a block depends on variables | | `engine.block.find(byName:)` | Find blocks by their semantic name | | `engine.block.findAllPlaceholders()` | Return every placeholder block in the scene | | `engine.block.getFill(_:)` | Get the fill block of a design block | | `engine.block.setString(_:property:value:)` | Set a string property value | | `engine.block.export(_:mimeType:)` | Export a block as image data | ## Next Steps - [Batch Processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/) — Automate generation of multiple designs from a template in a loop. - [Templating](https://img.ly/docs/cesdk/mac-catalyst/concepts/templating-f94385/) — Learn the template model behind variables and placeholders. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Automate Design Generation" description: "Populate a reusable template from application data and export the finished design with CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/automation/design-generation-98a99e/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/mac-catalyst/automation-715209/) > [Design Generation](https://img.ly/docs/cesdk/mac-catalyst/automation/design-generation-98a99e/) --- ```swift file=@cesdk_swift_examples/engine-guides-design-generation/DesignGeneration.swift reference-only import Foundation import IMGLYEngine struct DesignGenerationGuideResult { let pngData: Data let outputURL: URL let referencedVariableNames: [String] let variableValuesAfterSet: [String: String] let imageBlockCount: Int let imageFillType: String let replacementImageURL: URL let storedImageURL: URL } @MainActor func designGeneration(engine: Engine) async throws -> DesignGenerationGuideResult { let baseURL = try engine.guidesBaseURL let templateURL = try await makeDesignGenerationGuideTemplate(engine: engine, baseURL: baseURL) defer { try? FileManager.default.removeItem(at: templateURL) } @MainActor func loadDesignGenerationTemplate(engine: Engine, from templateURL: URL) async throws -> [DesignBlockID] { try await engine.scene.load( from: templateURL, overrideEditorConfig: true, waitForResources: true, ) return try engine.scene.getPages() } struct DesignGenerationGuideRecord { let firstName: String let lastName: String let address: String let city: String let imageURL: URL } struct ValidatedDesignGenerationGuideTemplate { let page: DesignBlockID let record: DesignGenerationGuideRecord let referencedVariableNames: [String] let imageBlock: DesignBlockID let imageFill: DesignBlockID let imageBlockCount: Int let imageFillType: String } enum DesignGenerationGuideError: LocalizedError { case expectedSinglePage(Int) case missingVariables([String]) case missingVariableReferences case namedImageCount(name: String, count: Int) case unexpectedFillType(String) var errorDescription: String? { switch self { case let .expectedSinglePage(count): "Expected the template to contain one page, found \(count)." case let .missingVariables(keys): "Template is missing required variables: \(keys.joined(separator: ", "))." case .missingVariableReferences: "Template text blocks do not reference any variables." case let .namedImageCount(name, count): "Expected one image block named \(name), found \(count)." case let .unexpectedFillType(type): "Expected the named image block to use an image fill, found \(type)." } } } @MainActor func validateDesignGenerationTemplate( engine: Engine, pages: [DesignBlockID], baseURL: URL, ) throws -> ValidatedDesignGenerationGuideTemplate { guard pages.count == 1, let page = pages.first else { throw DesignGenerationGuideError.expectedSinglePage(pages.count) } let record = DesignGenerationGuideRecord( firstName: "John", lastName: "Doe", address: "123 Main St.", city: "Anytown", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) let requiredVariableKeys = Set(["first_name", "last_name", "address", "city"]) let textBlocks = try engine.block.find(byType: .text) let hasVariableReferences = try textBlocks.contains { try engine.block.referencesAnyVariables($0) } guard hasVariableReferences else { throw DesignGenerationGuideError.missingVariableReferences } let variableTokenPattern = try NSRegularExpression(pattern: #"\{\{\s*([^{}]+?)\s*\}\}"#) let referencedVariableNames = Set(try textBlocks.flatMap { block -> [String] in let content = try engine.block.getString(block, property: "text/text") let range = NSRange(content.startIndex ..< content.endIndex, in: content) return variableTokenPattern.matches(in: content, range: range).compactMap { match in Range(match.range(at: 1), in: content).map { String(content[$0]).trimmingCharacters(in: .whitespacesAndNewlines) } } }) let missingVariableKeys = requiredVariableKeys.subtracting(referencedVariableNames).sorted() guard missingVariableKeys.isEmpty else { throw DesignGenerationGuideError.missingVariables(missingVariableKeys) } let imageBlockName = "profile-photo" let namedBlocks = engine.block.find(byName: imageBlockName) guard namedBlocks.count == 1, let namedBlock = namedBlocks.first else { throw DesignGenerationGuideError.namedImageCount(name: imageBlockName, count: namedBlocks.count) } let fill = try engine.block.getFill(namedBlock) let fillType = try engine.block.getType(fill) guard fillType == FillType.image.rawValue else { throw DesignGenerationGuideError.unexpectedFillType(fillType) } return ValidatedDesignGenerationGuideTemplate( page: page, record: record, referencedVariableNames: referencedVariableNames.sorted(), imageBlock: namedBlock, imageFill: fill, imageBlockCount: namedBlocks.count, imageFillType: fillType, ) } @MainActor func populateDesignGenerationText(engine: Engine, record: DesignGenerationGuideRecord) throws { try engine.variable.set(key: "first_name", value: record.firstName) try engine.variable.set(key: "last_name", value: record.lastName) try engine.variable.set(key: "address", value: record.address) try engine.variable.set(key: "city", value: record.city) } @MainActor func replaceDesignGenerationImage( engine: Engine, imageBlock: DesignBlockID, imageFill: DesignBlockID, imageURL: URL, ) throws -> URL { try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: imageURL, ) try engine.block.resetCrop(imageBlock) return try engine.block.getURL(imageFill, property: "fill/image/imageFileURI") } @MainActor func exportDesignGenerationPage(engine: Engine, page: DesignBlockID) async throws -> (Data, URL) { try await engine.block.forceLoadResources([page]) let pngData = try await engine.block.export(page, mimeType: .png) let outputURL = FileManager.default.temporaryDirectory .appendingPathComponent("personalized-design-\(UUID().uuidString).png") try pngData.write(to: outputURL, options: .atomic) return (pngData, outputURL) } let pages = try await loadDesignGenerationTemplate(engine: engine, from: templateURL) let validatedTemplate = try validateDesignGenerationTemplate(engine: engine, pages: pages, baseURL: baseURL) try populateDesignGenerationText(engine: engine, record: validatedTemplate.record) let storedImageURL = try replaceDesignGenerationImage( engine: engine, imageBlock: validatedTemplate.imageBlock, imageFill: validatedTemplate.imageFill, imageURL: validatedTemplate.record.imageURL, ) let (pngData, outputURL) = try await exportDesignGenerationPage(engine: engine, page: validatedTemplate.page) try await engine.captureGuide(data: pngData, label: "hero", mimeType: .png) let variableValuesAfterSet = [ "first_name": try engine.variable.get(key: "first_name"), "last_name": try engine.variable.get(key: "last_name"), "address": try engine.variable.get(key: "address"), "city": try engine.variable.get(key: "city"), ] return DesignGenerationGuideResult( pngData: pngData, outputURL: outputURL, referencedVariableNames: validatedTemplate.referencedVariableNames, variableValuesAfterSet: variableValuesAfterSet, imageBlockCount: validatedTemplate.imageBlockCount, imageFillType: validatedTemplate.imageFillType, replacementImageURL: validatedTemplate.record.imageURL, storedImageURL: storedImageURL, ) } @MainActor private func makeDesignGenerationGuideTemplate(engine: Engine, baseURL: URL) async throws -> URL { let scene = try engine.scene.create(designUnit: .px, fontSizeUnit: .px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1200) try engine.block.setHeight(page, value: 800) try engine.block.appendChild(to: scene, child: page) let pageFill = try engine.block.createFill(.color) try engine.block.setColor( pageFill, property: "fill/color/value", color: .rgba(r: 0.96, g: 0.97, b: 0.98, a: 1), ) try engine.block.setFill(page, fill: pageFill) let accent = try engine.block.create(.graphic) try engine.block.setShape(accent, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(accent, value: 0) try engine.block.setPositionY(accent, value: 0) try engine.block.setWidth(accent, value: 18) try engine.block.setHeight(accent, value: 800) let accentFill = try engine.block.createFill(.color) try engine.block.setColor( accentFill, property: "fill/color/value", color: .rgba(r: 0.04, g: 0.49, b: 0.47, a: 1), ) try engine.block.setFill(accent, fill: accentFill) try engine.block.appendChild(to: page, child: accent) let eyebrow = try engine.block.create(.text) try engine.block.replaceText(eyebrow, text: "PERSONALIZED DELIVERY") try engine.block.setPositionX(eyebrow, value: 80) try engine.block.setPositionY(eyebrow, value: 92) try engine.block.setWidth(eyebrow, value: 500) try engine.block.setHeightMode(eyebrow, mode: .auto) try engine.block.setTextFontSize(eyebrow, fontSize: 22) try engine.block.setTextColor(eyebrow, color: .rgba(r: 0.04, g: 0.49, b: 0.47, a: 1)) try engine.block.appendChild(to: page, child: eyebrow) let recipient = try engine.block.create(.text) try engine.block.replaceText(recipient, text: "{{first_name}}\n{{last_name}}") try engine.block.setPositionX(recipient, value: 80) try engine.block.setPositionY(recipient, value: 152) try engine.block.setWidth(recipient, value: 500) try engine.block.setHeightMode(recipient, mode: .auto) try engine.block.setTextFontSize(recipient, fontSize: 80) try engine.block.setTextColor(recipient, color: .rgba(r: 0.08, g: 0.11, b: 0.18, a: 1)) try engine.block.appendChild(to: page, child: recipient) let addressLabel = try engine.block.create(.text) try engine.block.replaceText(addressLabel, text: "SEND TO") try engine.block.setPositionX(addressLabel, value: 80) try engine.block.setPositionY(addressLabel, value: 500) try engine.block.setWidth(addressLabel, value: 500) try engine.block.setHeightMode(addressLabel, mode: .auto) try engine.block.setTextFontSize(addressLabel, fontSize: 18) try engine.block.setTextColor(addressLabel, color: .rgba(r: 0.42, g: 0.45, b: 0.51, a: 1)) try engine.block.appendChild(to: page, child: addressLabel) let address = try engine.block.create(.text) try engine.block.replaceText(address, text: "{{address}}\n{{city}}") try engine.block.setPositionX(address, value: 80) try engine.block.setPositionY(address, value: 548) try engine.block.setWidth(address, value: 500) try engine.block.setHeightMode(address, mode: .auto) try engine.block.setTextFontSize(address, fontSize: 32) try engine.block.setTextColor(address, color: .rgba(r: 0.08, g: 0.11, b: 0.18, a: 1)) try engine.block.appendChild(to: page, child: address) let imageBlock = try engine.block.create(.graphic) try engine.block.setName(imageBlock, name: "profile-photo") try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(imageBlock, value: 650) try engine.block.setPositionY(imageBlock, value: 72) try engine.block.setWidth(imageBlock, value: 470) try engine.block.setHeight(imageBlock, value: 656) try engine.block.setContentFillMode(imageBlock, mode: .cover) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.appendChild(to: page, child: imageBlock) try engine.variable.set(key: "first_name", value: "First") try engine.variable.set(key: "last_name", value: "Last") try engine.variable.set(key: "address", value: "Address") try engine.variable.set(key: "city", value: "City") try await engine.block.forceLoadResources([page]) let templateString = try await engine.scene.saveToString() let templateData = Data(templateString.utf8) let templateURL = FileManager.default.temporaryDirectory .appendingPathComponent("design-generation-template-\(UUID().uuidString).imgly") try templateData.write(to: templateURL, options: .atomic) // Ensure the following load, rather than this fixture setup, restores the serialized variables. let fixtureVariableKeys = Set(["first_name", "last_name", "address", "city"]) for key in fixtureVariableKeys where engine.variable.findAll().contains(key) { try engine.variable.remove(key: key) } return templateURL } ``` Populate a reusable template from application data and export the finished design as a PNG with the CE.SDK Engine API. ![A personalized delivery card populated with John Doe's name and address, with the profile photo replaced and recropped to cover its frame.](./assets/swift-based.hero.webp) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-design-generation) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. The workflow starts from one pristine template, validates its data contract, applies one record, and exports its page. Start every independent generation job from the original template input and resolve new block IDs after loading it. ## Load a Template Load a remote or bundled `.scene` file from a native `URL`. `overrideEditorConfig: true` imports serialized settings. The scene's variables load either way and replace the variables of the previous scene. `waitForResources: true` waits for initial resources before returning. ```swift highlight-designGeneration-load @MainActor func loadDesignGenerationTemplate(engine: Engine, from templateURL: URL) async throws -> [DesignBlockID] { try await engine.scene.load( from: templateURL, overrideEditorConfig: true, waitForResources: true, ) return try engine.scene.getPages() } ``` The example creates a local template fixture; production code can pass an HTTPS URL to the same overload. Every engine call runs under `@MainActor`. Treat each page, block, and fill ID as valid only for the scene that returned it. ## Map and Validate Data Map one application record to the template's expected fields, then validate the contract before changing engine data. Read each loaded text block's `text/text` property and extract its `{{token}}` keys. `engine.variable.findAll()` lists every variable of the scene, also one that no token uses, so it does not show which tokens the template uses. ```swift highlight-designGeneration-validate struct DesignGenerationGuideRecord { let firstName: String let lastName: String let address: String let city: String let imageURL: URL } struct ValidatedDesignGenerationGuideTemplate { let page: DesignBlockID let record: DesignGenerationGuideRecord let referencedVariableNames: [String] let imageBlock: DesignBlockID let imageFill: DesignBlockID let imageBlockCount: Int let imageFillType: String } enum DesignGenerationGuideError: LocalizedError { case expectedSinglePage(Int) case missingVariables([String]) case missingVariableReferences case namedImageCount(name: String, count: Int) case unexpectedFillType(String) var errorDescription: String? { switch self { case let .expectedSinglePage(count): "Expected the template to contain one page, found \(count)." case let .missingVariables(keys): "Template is missing required variables: \(keys.joined(separator: ", "))." case .missingVariableReferences: "Template text blocks do not reference any variables." case let .namedImageCount(name, count): "Expected one image block named \(name), found \(count)." case let .unexpectedFillType(type): "Expected the named image block to use an image fill, found \(type)." } } } @MainActor func validateDesignGenerationTemplate( engine: Engine, pages: [DesignBlockID], baseURL: URL, ) throws -> ValidatedDesignGenerationGuideTemplate { guard pages.count == 1, let page = pages.first else { throw DesignGenerationGuideError.expectedSinglePage(pages.count) } let record = DesignGenerationGuideRecord( firstName: "John", lastName: "Doe", address: "123 Main St.", city: "Anytown", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) let requiredVariableKeys = Set(["first_name", "last_name", "address", "city"]) let textBlocks = try engine.block.find(byType: .text) let hasVariableReferences = try textBlocks.contains { try engine.block.referencesAnyVariables($0) } guard hasVariableReferences else { throw DesignGenerationGuideError.missingVariableReferences } let variableTokenPattern = try NSRegularExpression(pattern: #"\{\{\s*([^{}]+?)\s*\}\}"#) let referencedVariableNames = Set(try textBlocks.flatMap { block -> [String] in let content = try engine.block.getString(block, property: "text/text") let range = NSRange(content.startIndex ..< content.endIndex, in: content) return variableTokenPattern.matches(in: content, range: range).compactMap { match in Range(match.range(at: 1), in: content).map { String(content[$0]).trimmingCharacters(in: .whitespacesAndNewlines) } } }) let missingVariableKeys = requiredVariableKeys.subtracting(referencedVariableNames).sorted() guard missingVariableKeys.isEmpty else { throw DesignGenerationGuideError.missingVariables(missingVariableKeys) } let imageBlockName = "profile-photo" let namedBlocks = engine.block.find(byName: imageBlockName) guard namedBlocks.count == 1, let namedBlock = namedBlocks.first else { throw DesignGenerationGuideError.namedImageCount(name: imageBlockName, count: namedBlocks.count) } let fill = try engine.block.getFill(namedBlock) let fillType = try engine.block.getType(fill) guard fillType == FillType.image.rawValue else { throw DesignGenerationGuideError.unexpectedFillType(fillType) } return ValidatedDesignGenerationGuideTemplate( page: page, record: record, referencedVariableNames: referencedVariableNames.sorted(), imageBlock: namedBlock, imageFill: fill, imageBlockCount: namedBlocks.count, imageFillType: fillType, ) } ``` The validation confirms every required text reference and that exactly one block named `profile-photo` owns an image fill. Failing early identifies a template mismatch instead of silently targeting an unrelated graphic. ## Populate Text Variables Set each case-sensitive variable key to its string value. Every text block that references a matching token uses the new value when rendered or exported. ```swift highlight-designGeneration-populateText @MainActor func populateDesignGenerationText(engine: Engine, record: DesignGenerationGuideRecord) throws { try engine.variable.set(key: "first_name", value: record.firstName) try engine.variable.set(key: "last_name", value: record.lastName) try engine.variable.set(key: "address", value: record.address) try engine.variable.set(key: "city", value: record.city) } ``` Use `engine.variable.get(key:)` for preflight checks when needed. Remove only keys your application owns; clearing unrelated variables can alter other template behavior. ## Replace a Named Image Reuse the named block's existing image fill and update its `fill/image/imageFileURI` property with a native `URL`. Resetting the crop recalculates how replacement media with different dimensions covers the graphic block. Reading the property back as a `URL` verifies the stored replacement. ```swift highlight-designGeneration-replaceImage @MainActor func replaceDesignGenerationImage( engine: Engine, imageBlock: DesignBlockID, imageFill: DesignBlockID, imageURL: URL, ) throws -> URL { try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: imageURL, ) try engine.block.resetCrop(imageBlock) return try engine.block.getURL(imageFill, property: "fill/image/imageFileURI") } ``` Do not create a replacement fill when the existing fill has the wrong type. That would hide a broken template contract and discard the fill configuration authored with the template. ## Export the Design After changing remote media, force the page's resources to load and export that page as PNG data. Persist the returned `Data` with `write(to:)`, upload it, or pass it to another native API. ```swift highlight-designGeneration-export @MainActor func exportDesignGenerationPage(engine: Engine, page: DesignBlockID) async throws -> (Data, URL) { try await engine.block.forceLoadResources([page]) let pngData = try await engine.block.export(page, mimeType: .png) let outputURL = FileManager.default.temporaryDirectory .appendingPathComponent("personalized-design-\(UUID().uuidString).png") try pngData.write(to: outputURL, options: .atomic) return (pngData, outputURL) } ``` The example generates one record, and its focused test verifies that the PNG decodes to non-blank pixels. Use `.jpeg` for a smaller raster result or `.pdf` for document output. For independent jobs, load the original template for each record and resolve fresh IDs; use [Data Merge](https://img.ly/docs/cesdk/mac-catalyst/automation/data-merge-ae087c/) for larger orchestration workflows. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.load(from:overrideEditorConfig:waitForResources:)` | Load a `.scene` template from a `URL` and return its scene ID. | | `engine.scene.getPages()` | Resolve pages from the currently loaded scene. | | `engine.variable.findAll()` | List the variables of the scene; a variable that no token uses is listed too. | | `engine.variable.set(key:value:)` | Set a string variable value. | | `engine.variable.get(key:)` | Read a variable value. | | `engine.variable.remove(key:)` | Remove an application-owned variable. | | `engine.block.find(byType:)` | Resolve text blocks with the type-safe overload. | | `engine.block.referencesAnyVariables(_:)` | Confirm that a text block references variables. | | `engine.block.getString(_:property:)` | Read `text/text` to validate its exact variable keys. | | `engine.block.find(byName:)` | Find blocks by their stable template name. | | `engine.block.getFill(_:)` | Read the fill attached to a block. | | `engine.block.getType(_:)` | Read the fill type for validation. | | `engine.block.setURL(_:property:value:)` | Set `fill/image/imageFileURI` with a native `URL`. | | `engine.block.getURL(_:property:)` | Read `fill/image/imageFileURI` as a native `URL`. | | `engine.block.resetCrop(_:)` | Reframe replaced media to cover its block. | | `engine.block.forceLoadResources(_:)` | Wait for changed page resources. | | `engine.block.export(_:mimeType:options:onPreExport:uriResolver:)` | Export the populated page as `Blob`, which is `Data`. | ## Next Steps - [Use Templates](https://img.ly/docs/cesdk/mac-catalyst/use-templates/overview-ae74e1/) - Prepare and load reusable templates. - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) - Work with variable-backed text. - [Data Merge](https://img.ly/docs/cesdk/mac-catalyst/automation/data-merge-ae087c/) - Process structured records and media placeholders. - [Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) - Configure output formats and quality. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Multiple Image Generation" description: "Create many image variants from structured data by interpolating content into reusable design templates." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/automation/multi-image-generation-2a0de4/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/mac-catalyst/automation-715209/) > [Multiple Image Generation](https://img.ly/docs/cesdk/mac-catalyst/automation/multi-image-generation-2a0de4/) --- Generate image variants, such as square, portrait, or landscape layouts, from a single data record using the CreativeEditor SDK’s Engine API. This pattern lets you populate templates programmatically with text, images, and colors to create consistent, on‑brand designs across all formats. ## What You’ll Learn - Load multiple templates into CE.SDK and populate them with structured data. - Replace text and image placeholders dynamically using variables and named blocks. - Apply consistent brand color themes across scenes. - Export each variant as PNG, JPEG, or PDF. - Build a SwiftUI preview for the generated images. ## When to Use It Use multi‑image generation when a single record (like a restaurant listing or product) needs to produce multiple layout variants. For larger datasets, many records generating many images, refer to the [Batch Processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/) guide. ## Core Concepts **Templates and Instances**: Templates define reusable layout and placeholders. An instance is a populated version with specific data. Use `scene.saveToString()` to serialize a template and `scene.load(from:)` to load it for processing. **Variables for Dynamic Text**: Define variables in your templates for fields like `RestaurantName` or `Rating`. Set them at runtime with `engine.variable.setString(name:value:)`. Use `engine.variable.findAll()` to verify available variable names. **Named Blocks for Image Replacement**: Name your image placeholders (for example, `RestaurantImage`, `Logo`). Retrieve them with `engine.block.findByName()`, access the fill with `getFill()`, then update its source URI using `setString(..., property: "fill/image/imageFileURI")`. Always reset the crop after replacing an image fill for proper framing. **Brand and Conditional Styling**: Use predictable block naming for elements such as star ratings. Apply color changes programmatically with `setTextColor` or `setColor` to visualize rating or brand status. **Sequential Template Processing**: Process each variant one at a time to reduce memory pressure and simplify export tracking. ## Prerequisites - CE.SDK for iOS integrated through Swift Package Manager. - A valid license key. - Templates archived as `.scene` or `.archive` files. - Template variables and named blocks prepared for population. ## Initialize the Engine ```swift import IMGLYEngine import IMGLYCore @MainActor func makeEngine() async throws -> Engine { let engine = try Engine(license: "") let baseURL = URL(string: "https://cdn.img.ly/packages/imgly/cesdk-swift/1.86.2/assets")! let sourceIDs = [ "ly.img.sticker", "ly.img.vector.shape", "ly.img.filter", "ly.img.color.palette", "ly.img.effect", "ly.img.blur", "ly.img.typeface", "ly.img.crop.presets", "ly.img.page.presets", "ly.img.text", "ly.img.text.components", ] for id in sourceIDs { try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent(id).appendingPathComponent("content.json") ) } return engine } ``` ## Define Your Data Model Your data model can use proper typing for variables. When you insert values into the templates, you will often need to convert them to strings. ```swift struct Restaurant: Identifiable, Sendable { let id: UUID let name: String let rating: Double let reviewCount: Int let imageURL: String let logoURL: String let brandPrimary: String let brandSecondary: String } ``` This model provides a data record for the example code below. ## Populate Templates and Export Variants Use one template per format such as: - square - portrait - landscape Populate the templates sequentially. ```swift @MainActor func generateVariants(engine: Engine, for restaurant: Restaurant) async throws -> [URL] { let templates = [ "restaurant_square", "restaurant_portrait", "restaurant_landscape" ].compactMap { Bundle.main.url(forResource: $0, withExtension: "scene") } var results: [URL] = [] for template in templates { let scene = try await engine.scene.load(from: template) // Set text variables try engine.variable.setString("RestaurantName", value: restaurant.name) try engine.variable.setString("Rating", value: String(format: "%.1f ★", restaurant.rating)) try engine.variable.setString("ReviewCount", value: "\(restaurant.reviewCount)") // Replace images try replaceImage(engine: engine, name: "RestaurantImage", with: restaurant.imageURL) try replaceImage(engine: engine, name: "Logo", with: restaurant.logoURL) // Apply brand theme try applyBrandTheme(engine: engine, primary: Color.fromHex(restaurant.brandPrimary), secondary: Color.fromHex(restaurant.brandSecondary)) // Export variant let output = try await exportJPEG(engine: engine, name: outputName(for: restaurant, template: template)) results.append(output) } return results } ``` **Helper Functions**: The preceding code example uses some helper functions. These aren’t part of the CE.SDK. Possible implementations of the functions follow. The function for `Color.fromHex()` is at the end of the guide as it’s used in another example as well. ```swift private func replaceImage(engine: Engine, name: String, with uri: String) throws { if let block = engine.block.find(byName: name).first { let fill = try engine.block.getFill(block) try engine.block.setString(fill, property: "fill/image/fileURI", value: uri) try engine.block.resetCrop(fill) } } private func applyBrandTheme(engine: Engine, primary: Color, secondary: Color) throws { for block in try engine.block.findAll() { switch try engine.block.getType(block) { case "//ly.img.ubq/text": try engine.block.setTextColor(block, color: primary) case "//ly.img.ubq/graphic": if let fill = try? engine.block.getFill(block) { try engine.block.setColor(fill, property: "fill/color/value", color: secondary) } default: break } } } private func exportJPEG(engine: Engine, name: String) async throws -> URL { guard let page = try engine.block.find(byType: .page).first else { throw NSError(domain: "no-page", code: 1) } let data = try await engine.block.export(page, mimeType: .jpeg) let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let url = dir.appendingPathComponent("\(name).jpg") try data.write(to: url, options: .atomic) return url } ``` ## Preview the Generated Variants Use SwiftUI to display and share generated images. ```swift struct VariantsGrid: View { let urls: [URL] @State private var shareURL: URL? var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 160), spacing: 12)]) { ForEach(urls, id: \.self) { url in if let image = UIImage(contentsOfFile: url.path) { Button { shareURL = url } label: { Image(uiImage: image) .resizable().scaledToFit() .clipShape(RoundedRectangle(cornerRadius: 10)) .shadow(radius: 2) } } } }.padding() } .sheet(item: $shareURL) { url in ShareLink(items: [url]) { Text("Share \(url.lastPathComponent)") } } } } ``` ## Advanced Use Cases **Conditional Content**: Show or hide elements based on data values—for example, color stars according to the rating. ```swift func colorStars(engine: Engine, rating: Int, baseName: String = "Rating") throws { for index in 1...5 { guard let star = try engine.block.find(byName:"\(baseName)\(index)").first, let fill = try? engine.block.getFill(star) else { continue } let color = index <= rating ? Color.hex("#FFD60A") : Color.hex("#CCCCCC") try engine.block.setColor(fill, property: "fill/color/value", color: color) } } ``` **Custom Assets**: Add your own logos or fonts by registering a custom asset source. See the [Unsplash](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/) guide for a complete custom asset source example covering search, pagination, and asset mapping. **Adopter Mode Editing** Allow users to open the generated design in the editor UI for minor edits. Serialize the populated scene with `scene.saveToString()` and load it into the Design Editor configured for [restricted content](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) editing. ## Troubleshooting **❌ Variables not updating**: - Verify variable names in both template and code. **❌ Images missing**: - Confirm local path or remote URL points to a valid image. **❌ Colors incorrect**: - Check block type before applying color. **❌ Memory spikes**: - Process templates sequentially. **❌ Export size unexpected**: - Confirm consistent `page`, `scene` and `block` dimensions across templates. **Debugging Tips**: - Print variable names using `engine.variable.findAll()` - Log block names with `engine.block.getName(id)` - Test with one minimal template before expanding ## Next Steps Multi-image generation is one way to automate your workflow. Some other ways the CE.SDK can automate are in these guides: - [Batch Processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/) lets you process many data records at once. - Adapt layouts across aspect ratios using [auto resize](https://img.ly/docs/cesdk/mac-catalyst/automation/auto-resize-4c2d58/). - Explore [export formats](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) and settings. - Add branded fonts, logos, and graphics by creating custom asset sources, following the [Unsplash](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/) example. *** ## Utility Extension Add this helper to convert hex strings into CE.SDK `Color` values. Use it in the guide examples as `Color.fromHex("#FFD60A")` or `Color.fromHex(restaurant.brandPrimary)`. ```swift import IMGLYCore extension Color { /// Create a CE.SDK Color from a hex string like "#FFAA33" or "#FFAA33FF" static func fromHex(_ hex: String) -> Color { var hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines) .replacingOccurrences(of: "#", with: "") if hexString.count == 6 { hexString.append("FF") } // add alpha if missing var hexValue: UInt64 = 0 Scanner(string: hexString).scanHexInt64(&hexValue) let r = Float((hexValue & 0xFF000000) >> 24) / 255.0 let g = Float((hexValue & 0x00FF0000) >> 16) / 255.0 let b = Float((hexValue & 0x0000FF00) >> 8) / 255.0 let a = Float((hexValue & 0x000000FF)) / 255.0 return Color(r: r, g: g, b: b, a: a) } } ``` --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Overview" description: "Automate repetitive editing tasks using CE.SDK’s headless APIs to generate assets at scale." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/automation/overview-34d971/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/mac-catalyst/automation-715209/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/automation/overview-34d971/) --- Workflow automation with CreativeEditor SDK (CE.SDK) enables you to programmatically generate, manipulate, and export creative assets—at scale. Whether you're creating thousands of localized ads, preparing platform-specific variants of a campaign, or populating print-ready templates with dynamic data, CE.SDK provides a flexible foundation for automation. You can run automation entirely on the client, integrate it with your backend, or build hybrid “human-in-the-loop” workflows where users interact with partially automated scenes before export. The automation engine supports static pipelines, making it suitable for a wide range of publishing, e-commerce, and marketing applications. Video support will follow soon. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## What Can Be Automated with CE.SDK CE.SDK supports a wide variety of automation use cases, including: - **Design generation at scale**: Create thousands of variants from a single template, such as product cards or regionalized campaigns. - **Data-driven customization**: Merge external data (e.g., CSV, JSON, APIs) into templates to personalize text, images, or layout. - **Responsive output creation**: Automatically resize designs and export assets in different aspect ratios or dimensions for various platforms. - **Pre-export validation**: Detect issues like empty placeholders or low-resolution images before generating final output. - **Multimodal exporting**: Automate delivery to multiple formats including JPG, PNG, PDF, and MP4. ## Automation Contexts ### Headless / Server-Side Automation Server-side automation provides complete control over content generation without rendering a UI. This is ideal for background processing, such as creating assets in response to API requests or batch-generating print files for a mail campaign. ### UI-Integrated Automation (Human-in-the-Loop) For workflows that require user input or final approval, you can embed automation into the CE.SDK UI. Users can review, customize, or finalize designs that were pre-filled with dynamic data—ideal for marketing teams, e-commerce admins, or print professionals. ### Client-Side vs. Backend-Supported Workflows Many automation workflows can run directly on the client or device thanks to CE.SDK’s client-side architecture. However, a backend may be required for use cases involving: - Secure access to private assets - Large dataset lookups - Server-side template rendering - Scheduled or event-based triggers ## Customization Capabilities CE.SDK gives you deep control over how your automation pipeline behaves: ### Data Sources Connect to a variety of inputs: - Local or remote JSON - CSV files - REST APIs - CMS or PIM systems ### Template Customization - Define dynamic variables and conditional placeholders - Use reusable templates or generate them on-the-fly - Lock or constrain specific fields to preserve brand integrity ### Design Rules Enforce visual and content constraints: - Brand-compliant colors and fonts - Overflow handling and text auto-resizing - Show/hide conditions and fallback logic ### Output Formats | Category | Supported Formats | | ----------- | ------------------------------------------------------------------------------------ | | **Images** | `.png` (with transparency), `.jpeg`, `.webp`, `.tga` | | **Vector** | `.svg` (scalable vector graphics with text as paths) | | **Print** | `.pdf` (supports underlayer printing and spot colors) | | **Video** | `.mp4` (H.264 or H.265 on supported platforms with limited transparency support), `.webm` (VP8, VP9 or AV1 with Opus or Vorbis audio; VP8 and VP9 keep transparency on Android, iOS and macOS) | | **Scene** | `.imgly` or `.scene` (description of the scene without any assets) | | **Archive** | `.imgly` or `.zip` (fully self-contained archive that bundles the scene file with all assets) | > **Note:** Our custom cross-platform C++ based rendering and layout engine ensures > consistent output quality across devices. ## UI Customization for Automation You can extend the CE.SDK UI to trigger and manage automation tasks directly in the interface: - Add buttons or panels to trigger workflows - Dynamically update the scene based on user input or external data - Customize visibility of UI components depending on the stage (e.g., pre-fill vs. review) This makes it easy to integrate human-in-the-loop flows while preserving a tailored editing experience. ## Mapping Your Use Case Use the table below to find the right guide for your automation scenario: | Use Case | Relevant Guide | | --- | --- | | Create many images from one template | [Batch Processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/) | | Merge CSV or JSON data with a template | [Data Merge](https://img.ly/docs/cesdk/mac-catalyst/automation/data-merge-ae087c/) | | Generate platform-specific size variants | [Product Variations](https://img.ly/docs/cesdk/mac-catalyst/automation/product-variations-f3349f/) | | Resize designs for different aspect ratios | [Auto-Resize](https://img.ly/docs/cesdk/mac-catalyst/automation/auto-resize-4c2d58/) | | Build scenes programmatically | [Automate Design Generation](https://img.ly/docs/cesdk/mac-catalyst/automation/design-generation-98a99e/) | | Generate multiple images per design | [Multiple Image Generation](https://img.ly/docs/cesdk/mac-catalyst/automation/multi-image-generation-2a0de4/) | | Trigger automation from the editor UI | Actions | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Product Variations" description: "Generate multiple product variants from a single template by swapping text, images and styles programmatically." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/automation/product-variations-f3349f/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Automate Workflows](https://img.ly/docs/cesdk/mac-catalyst/automation-715209/) > [Product Variations](https://img.ly/docs/cesdk/mac-catalyst/automation/product-variations-f3349f/) --- ```swift file=@cesdk_swift_examples/engine-guides-product-variations/ProductVariations.swift reference-only import Foundation import IMGLYEngine @MainActor func productVariations(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL struct ProductVariant { let color: String let size: String let price: String let imageURL: URL } let variants: [ProductVariant] = [ ProductVariant( color: "Midnight Black", size: "M", price: "$29.99", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ), ProductVariant( color: "Ocean Blue", size: "L", price: "$34.99", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ), ] let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 500) try engine.block.setHeight(page, value: 500) let text = try engine.block.create(.text) try engine.block.appendChild(to: page, child: text) try engine.block.setWidth(text, value: 400) try engine.block.setHeight(text, value: 50) try engine.block.setPositionX(text, value: 50) try engine.block.setPositionY(text, value: 50) try engine.block.replaceText(text, text: "{{ProductName}} – {{ProductColor}}") let priceText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: priceText) try engine.block.setWidth(priceText, value: 200) try engine.block.setHeight(priceText, value: 40) try engine.block.setPositionX(priceText, value: 50) try engine.block.setPositionY(priceText, value: 120) try engine.block.replaceText(priceText, text: "{{ProductPrice}}") let imageBlock = try engine.block.create(.graphic) try engine.block.appendChild(to: page, child: imageBlock) try engine.block.setWidth(imageBlock, value: 300) try engine.block.setHeight(imageBlock, value: 300) try engine.block.setPositionX(imageBlock, value: 100) try engine.block.setPositionY(imageBlock, value: 180) try engine.block.setName(imageBlock, name: "ProductImage") let imageFill = try engine.block.createFill(.image) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) // Save template as a string for further export let templateString = try await engine.scene.saveToString() let variableKeys = engine.variable.findAll() print("Template variables: \(variableKeys)") // Expected: ["ProductName", "ProductColor", "ProductPrice"] for variant in variants { // Reload the template for each variant try await engine.scene.load(from: templateString) // Set text variables for this variant try engine.variable.set(key: "ProductName", value: "Classic Tee") try engine.variable.set(key: "ProductColor", value: variant.color) try engine.variable.set(key: "ProductPrice", value: variant.price) // Replace the product image by finding the block by name if let block = engine.block.find(byName: "ProductImage").first { let fill = try engine.block.getFill(block) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: variant.imageURL, ) } // Export the current variation guard let exportPage = try engine.block.find(byType: .page).first else { continue } let blob = try await engine.block.export(exportPage, mimeType: .jpeg) // Save to a file let dir = FileManager.default.temporaryDirectory let fileName = "product-\(variant.color.lowercased().replacingOccurrences(of: " ", with: "-"))-\(variant.size).jpg" let fileURL = dir.appendingPathComponent(fileName) try blob.write(to: fileURL) } } ``` Generate multiple product variants — different colors, sizes or copy — from a single design template using the CE.SDK Engine API in Swift. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-product-variations) ## What You'll Learn - Define a data model to describe product attribute combinations. - Load a design template and discover its available variables. - Replace text placeholders and swap product images for each variant. - Export each variation as JPEG. ## When to Use It Use product variations when a single product needs multiple visual representations — for example, a t-shirt in five colors or a sneaker in three sizes. Each variant shares the same layout but differs in text, images or styling. For generating many images from **different data records**, see [Batch Processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/). For producing **multiple layout formats** from one record, see [Multiple Image Generation](https://img.ly/docs/cesdk/mac-catalyst/automation/multi-image-generation-2a0de4/). ## Define the Data Model Start by modeling your product variants. Each entry represents one combination of attributes to apply to the template. ```swift highlight-productVariations-dataModel struct ProductVariant { let color: String let size: String let price: String let imageURL: URL } let variants: [ProductVariant] = [ ProductVariant( color: "Midnight Black", size: "M", price: "$29.99", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ), ProductVariant( color: "Ocean Blue", size: "L", price: "$34.99", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ), ] ``` The `imageURL` points to the product photo for that color variant. In production, you'd load these from an API or database. ## Create a Template Product variation templates use **text variables** (wrapped in `{{double braces}}`) and **named image blocks** as placeholders. You can create templates in the Web CE.SDK editor and save them as archives, or build them on any platform programmatically (see example below). ```swift highlight-productVariations-createTemplate let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 500) try engine.block.setHeight(page, value: 500) let text = try engine.block.create(.text) try engine.block.appendChild(to: page, child: text) try engine.block.setWidth(text, value: 400) try engine.block.setHeight(text, value: 50) try engine.block.setPositionX(text, value: 50) try engine.block.setPositionY(text, value: 50) try engine.block.replaceText(text, text: "{{ProductName}} – {{ProductColor}}") let priceText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: priceText) try engine.block.setWidth(priceText, value: 200) try engine.block.setHeight(priceText, value: 40) try engine.block.setPositionX(priceText, value: 50) try engine.block.setPositionY(priceText, value: 120) try engine.block.replaceText(priceText, text: "{{ProductPrice}}") let imageBlock = try engine.block.create(.graphic) try engine.block.appendChild(to: page, child: imageBlock) try engine.block.setWidth(imageBlock, value: 300) try engine.block.setHeight(imageBlock, value: 300) try engine.block.setPositionX(imageBlock, value: 100) try engine.block.setPositionY(imageBlock, value: 180) try engine.block.setName(imageBlock, name: "ProductImage") let imageFill = try engine.block.createFill(.image) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) // Save template as a string for further export let templateString = try await engine.scene.saveToString() ``` Text blocks containing `{{ProductName}}`, `{{ProductColor}}` and `{{ProductPrice}}` automatically register as variables. Named image blocks like `"ProductImage"` let you swap fills by name. ## Discover Template Variables Before processing, verify which variables the template exposes. This is useful for validating data against the template at runtime. ```swift highlight-productVariations-discoverVariables let variableKeys = engine.variable.findAll() print("Template variables: \(variableKeys)") // Expected: ["ProductName", "ProductColor", "ProductPrice"] ``` `findAll()` returns the keys of all registered text variables. Use this to confirm your data model covers every placeholder before starting a batch run. ## Generate Variations Loop through each variant, reload the template, populate variables, then export. ```swift highlight-productVariations-generateLoop for variant in variants { // Reload the template for each variant try await engine.scene.load(from: templateString) // Set text variables for this variant try engine.variable.set(key: "ProductName", value: "Classic Tee") try engine.variable.set(key: "ProductColor", value: variant.color) try engine.variable.set(key: "ProductPrice", value: variant.price) // Replace the product image by finding the block by name if let block = engine.block.find(byName: "ProductImage").first { let fill = try engine.block.getFill(block) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: variant.imageURL, ) } // Export the current variation guard let exportPage = try engine.block.find(byType: .page).first else { continue } let blob = try await engine.block.export(exportPage, mimeType: .jpeg) // Save to a file let dir = FileManager.default.temporaryDirectory let fileName = "product-\(variant.color.lowercased().replacingOccurrences(of: " ", with: "-"))-\(variant.size).jpg" let fileURL = dir.appendingPathComponent(fileName) try blob.write(to: fileURL) } ``` ### Set Text Variables Use `engine.variable.set(key:value:)` to replace each placeholder with the variant's data: ```swift highlight-productVariations-setVariables // Set text variables for this variant try engine.variable.set(key: "ProductName", value: "Classic Tee") try engine.variable.set(key: "ProductColor", value: variant.color) try engine.variable.set(key: "ProductPrice", value: variant.price) ``` All variable values are strings. Convert numbers or prices to their display format before setting them. ### Replace the Product Image Find the image block by its name and update its fill URI: ```swift highlight-productVariations-replaceImage // Replace the product image by finding the block by name if let block = engine.block.find(byName: "ProductImage").first { let fill = try engine.block.getFill(block) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: variant.imageURL, ) } ``` The block name `"ProductImage"` was assigned when the template was created. Using names keeps automation readable compared to referencing block IDs directly. ### Export the Variant Export the populated page as JPEG and write it to disk: ```swift highlight-productVariations-export // Export the current variation guard let exportPage = try engine.block.find(byType: .page).first else { continue } let blob = try await engine.block.export(exportPage, mimeType: .jpeg) // Save to a file let dir = FileManager.default.temporaryDirectory let fileName = "product-\(variant.color.lowercased().replacingOccurrences(of: " ", with: "-"))-\(variant.size).jpg" let fileURL = dir.appendingPathComponent(fileName) try blob.write(to: fileURL) ``` You can export as PNG, PDF or other formats by changing the `mimeType` parameter. See the [Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) guide for all available options. ## Next Steps Product variations are one pattern for automating design output. Explore related guides: - [Batch Processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/) — process many data records at once. - [Multiple Image Generation](https://img.ly/docs/cesdk/mac-catalyst/automation/multi-image-generation-2a0de4/) — create multiple layout formats from one record. - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) — deep dive into the variable system. - [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) — work with placeholder blocks. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Capabilities" description: "Explore the full list of CE.SDK capabilities available for your platform, including design, video, image, text, and more." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/capabilities-e1906f/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) > [Capabilities](https://img.ly/docs/cesdk/mac-catalyst/capabilities-e1906f/) --- A comprehensive overview of all CE.SDK capabilities available for Mac Catalyst. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Colors" description: "Manage color usage in your designs, from applying brand palettes to handling print and screen formats." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/colors/overview-16a177/) - Manage color usage in your designs, from applying brand palettes to handling print and screen formats. - [Color Basics](https://img.ly/docs/cesdk/mac-catalyst/colors/basics-307115/) - Learn how color works in CE.SDK, including the three supported color spaces (sRGB, CMYK, and Spot) and when to use each for screen display or print workflows. - [For Print](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print-59bc05/) - Use print-ready color models and settings for professional-quality, production-ready exports. - [For Screen](https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen-1911f8/) - Documentation for For Screen - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) - Apply solid colors to design elements using CE.SDK's sRGB, CMYK, and spot color system. - [Replace Individual Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/replace-48cd71/) - Documentation for Replace Individual Colors - [Adjust Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/adjust-590d1e/) - Documentation for Adjust Colors - [Extract Dominant Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/extract-colors-d4c0a1/) - Read the most prominent colors from the rendered appearance of a block with the CE.SDK engine on Apple platforms. - [Color Conversion](https://img.ly/docs/cesdk/mac-catalyst/colors/conversion-bcd82b/) - Convert colors between sRGB, CMYK, and spot color spaces programmatically in CE.SDK for Swift. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Adjust Colors" description: "Documentation for Adjust Colors" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/adjust-590d1e/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [Adjust Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/adjust-590d1e/) --- Fine-tune images and graphics programmatically using CE.SDK's color adjustments system to control brightness, contrast, saturation, and other visual properties. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-colors-adjust) Color adjustments modify the visual appearance of images and graphics by changing properties like brightness, contrast, saturation, and color temperature. CE.SDK implements color adjustments as an `adjustments` effect type that attaches to compatible blocks. ```swift file=@cesdk_swift_examples/engine-guides-colors-adjust/ColorsAdjust.swift reference-only import Foundation import IMGLYEngine @MainActor func colorsAdjust(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Resolve sample assets against the bundled asset base URL. let baseURL = try engine.guidesBaseURL let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(imageBlock, value: 400) try engine.block.setHeight(imageBlock, value: 300) try engine.block.setPositionX(imageBlock, value: 200) try engine.block.setPositionY(imageBlock, value: 150) try engine.block.appendChild(to: page, child: imageBlock) let imageFill = try engine.block.createFill(.image) try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(imageBlock, fill: imageFill) // Not every block type supports effects. Pages return false, while image and // graphic blocks return true. let supportsEffects = try engine.block.supportsEffects(imageBlock) print("Block supports effects: \(supportsEffects)") // Create an adjustments effect and attach it to the image block. A block can // hold one adjustments effect in its effect stack; it exposes every color // adjustment property through a single effect instance. let adjustmentsEffect = try engine.block.createEffect(.adjustments) try engine.block.appendEffect(imageBlock, effectID: adjustmentsEffect) // Each adjustment property uses the "effect/adjustments/" prefix followed by // the property name. try engine.block.setFloat(adjustmentsEffect, property: "effect/adjustments/brightness", value: 0.4) try engine.block.setFloat(adjustmentsEffect, property: "effect/adjustments/contrast", value: 0.35) try engine.block.setFloat(adjustmentsEffect, property: "effect/adjustments/saturation", value: 0.5) try engine.block.setFloat(adjustmentsEffect, property: "effect/adjustments/temperature", value: 0.25) // Read a single adjustment value with getFloat, or list every property on the // adjustments effect with findAllProperties. let brightness = try engine.block.getFloat(adjustmentsEffect, property: "effect/adjustments/brightness") print("Current brightness: \(brightness)") let allProperties = try engine.block.findAllProperties(adjustmentsEffect) print("Available adjustment properties: \(allProperties)") // Toggle the adjustments effect without removing it. The values remain // attached; only rendering is suppressed while disabled. try engine.block.setEffectEnabled(effectID: adjustmentsEffect, enabled: false) let isEnabled = try engine.block.isEffectEnabled(effectID: adjustmentsEffect) print("Adjustments enabled: \(isEnabled)") try engine.block.setEffectEnabled(effectID: adjustmentsEffect, enabled: true) // Combine adjustments to create a distinct visual style. Here we build a // moody look with darker brightness, higher contrast, and lower saturation. let secondImageBlock = try engine.block.create(.graphic) try engine.block.setShape(secondImageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(secondImageBlock, value: 200) try engine.block.setHeight(secondImageBlock, value: 150) try engine.block.setPositionX(secondImageBlock, value: 50) try engine.block.setPositionY(secondImageBlock, value: 50) try engine.block.appendChild(to: page, child: secondImageBlock) let secondFill = try engine.block.createFill(.image) try engine.block.setURL(secondFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(secondImageBlock, fill: secondFill) let combinedAdjustments = try engine.block.createEffect(.adjustments) try engine.block.appendEffect(secondImageBlock, effectID: combinedAdjustments) try engine.block.setFloat(combinedAdjustments, property: "effect/adjustments/brightness", value: -0.15) try engine.block.setFloat(combinedAdjustments, property: "effect/adjustments/contrast", value: 0.4) try engine.block.setFloat(combinedAdjustments, property: "effect/adjustments/saturation", value: -0.3) let effects = try engine.block.getEffects(secondImageBlock) print("Effects on second image: \(effects.count)") // Refinement properties target image detail and tonal balance rather than // global color shifts. let tempBlock = try engine.block.create(.graphic) try engine.block.setShape(tempBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(tempBlock, value: 150) try engine.block.setHeight(tempBlock, value: 100) try engine.block.setPositionX(tempBlock, value: 550) try engine.block.setPositionY(tempBlock, value: 50) try engine.block.appendChild(to: page, child: tempBlock) let tempFill = try engine.block.createFill(.image) try engine.block.setURL(tempFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(tempBlock, fill: tempFill) let refinementEffect = try engine.block.createEffect(.adjustments) try engine.block.appendEffect(tempBlock, effectID: refinementEffect) try engine.block.setFloat(refinementEffect, property: "effect/adjustments/sharpness", value: 0.4) try engine.block.setFloat(refinementEffect, property: "effect/adjustments/clarity", value: 0.35) try engine.block.setFloat(refinementEffect, property: "effect/adjustments/highlights", value: -0.2) try engine.block.setFloat(refinementEffect, property: "effect/adjustments/shadows", value: 0.3) // Remove an effect by its index in the stack, then destroy the returned // effect block to free its resources. let tempEffects = try engine.block.getEffects(tempBlock) if let effectIndex = tempEffects.firstIndex(of: refinementEffect) { try engine.block.removeEffect(tempBlock, index: effectIndex) } try engine.block.destroy(refinementEffect) } ``` This guide covers how to apply color adjustments programmatically using the block API on iOS, macOS, and Mac Catalyst. ## Setup We start with a scene, a page, and an image block that we will adjust. The image is supplied as a remote URI on the fill; the engine fetches it on render. ```swift highlight-colorsAdjust-setup let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(imageBlock, value: 400) try engine.block.setHeight(imageBlock, value: 300) try engine.block.setPositionX(imageBlock, value: 200) try engine.block.setPositionY(imageBlock, value: 150) try engine.block.appendChild(to: page, child: imageBlock) let imageFill = try engine.block.createFill(.image) try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(imageBlock, fill: imageFill) ``` ## Check Block Compatibility Before applying adjustments, we verify the block supports effects. Page blocks don't support effects directly, while image and graphic blocks do. ```swift highlight-colorsAdjust-checkSupport // Not every block type supports effects. Pages return false, while image and // graphic blocks return true. let supportsEffects = try engine.block.supportsEffects(imageBlock) print("Block supports effects: \(supportsEffects)") ``` ## Create and Apply Adjustments Effect Once we've confirmed a block supports effects, we create an adjustments effect with `createEffect(.adjustments)` and attach it to the block using `appendEffect`. ```swift highlight-colorsAdjust-createAdjustments // Create an adjustments effect and attach it to the image block. A block can // hold one adjustments effect in its effect stack; it exposes every color // adjustment property through a single effect instance. let adjustmentsEffect = try engine.block.createEffect(.adjustments) try engine.block.appendEffect(imageBlock, effectID: adjustmentsEffect) ``` Each block can have one adjustments effect in its effect stack. The adjustments effect exposes every color adjustment property through a single effect instance. ## Modify Adjustment Properties We set individual adjustment values using `setFloat` with the effect block ID and a property path. Each property uses the `effect/adjustments/` prefix followed by the property name. ```swift highlight-colorsAdjust-setProperties // Each adjustment property uses the "effect/adjustments/" prefix followed by // the property name. try engine.block.setFloat(adjustmentsEffect, property: "effect/adjustments/brightness", value: 0.4) try engine.block.setFloat(adjustmentsEffect, property: "effect/adjustments/contrast", value: 0.35) try engine.block.setFloat(adjustmentsEffect, property: "effect/adjustments/saturation", value: 0.5) try engine.block.setFloat(adjustmentsEffect, property: "effect/adjustments/temperature", value: 0.25) ``` CE.SDK provides the following adjustment properties: | Property | Description | |----------|-------------| | `brightness` | Overall lightness—positive values lighten, negative values darken | | `contrast` | Tonal range—increases or decreases the difference between light and dark | | `saturation` | Color intensity—positive values increase vibrancy, negative values desaturate | | `exposure` | Exposure compensation—simulates camera exposure adjustments | | `gamma` | Gamma curve—adjusts midtone brightness | | `highlights` | Bright area intensity—controls the lightest parts of the image | | `shadows` | Dark area intensity—controls the darkest parts of the image | | `whites` | White point—adjusts the brightest pixels | | `blacks` | Black point—adjusts the darkest pixels | | `temperature` | Warm/cool color cast—positive for warmer, negative for cooler tones | | `sharpness` | Edge sharpness—enhances or softens edges | | `clarity` | Midtone contrast—increases local contrast for more definition | All properties accept `Float` values. Experiment with different values to achieve the desired visual result. ## Read Adjustment Values We read current adjustment values using `getFloat` with the same property paths. Use `findAllProperties` to discover every property available on an adjustments effect. ```swift highlight-colorsAdjust-readValues // Read a single adjustment value with getFloat, or list every property on the // adjustments effect with findAllProperties. let brightness = try engine.block.getFloat(adjustmentsEffect, property: "effect/adjustments/brightness") print("Current brightness: \(brightness)") let allProperties = try engine.block.findAllProperties(adjustmentsEffect) print("Available adjustment properties: \(allProperties)") ``` This is useful when building custom controls or syncing adjustment values across your application. ## Enable and Disable Adjustments CE.SDK allows you to toggle adjustments on and off without removing them from the block. This is useful for before/after comparisons or conditional processing. ```swift highlight-colorsAdjust-enableDisable // Toggle the adjustments effect without removing it. The values remain // attached; only rendering is suppressed while disabled. try engine.block.setEffectEnabled(effectID: adjustmentsEffect, enabled: false) let isEnabled = try engine.block.isEffectEnabled(effectID: adjustmentsEffect) print("Adjustments enabled: \(isEnabled)") try engine.block.setEffectEnabled(effectID: adjustmentsEffect, enabled: true) ``` When you disable an adjustments effect, it remains attached to the block but is not rendered until you re-enable it. All adjustment values are preserved. ## Applying Different Adjustment Styles You can apply different adjustment combinations to create distinct visual styles. The example below builds a moody look using negative brightness, high contrast, and desaturation. ```swift highlight-colorsAdjust-combineEffects // Combine adjustments to create a distinct visual style. Here we build a // moody look with darker brightness, higher contrast, and lower saturation. let secondImageBlock = try engine.block.create(.graphic) try engine.block.setShape(secondImageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(secondImageBlock, value: 200) try engine.block.setHeight(secondImageBlock, value: 150) try engine.block.setPositionX(secondImageBlock, value: 50) try engine.block.setPositionY(secondImageBlock, value: 50) try engine.block.appendChild(to: page, child: secondImageBlock) let secondFill = try engine.block.createFill(.image) try engine.block.setURL(secondFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(secondImageBlock, fill: secondFill) let combinedAdjustments = try engine.block.createEffect(.adjustments) try engine.block.appendEffect(secondImageBlock, effectID: combinedAdjustments) try engine.block.setFloat(combinedAdjustments, property: "effect/adjustments/brightness", value: -0.15) try engine.block.setFloat(combinedAdjustments, property: "effect/adjustments/contrast", value: 0.4) try engine.block.setFloat(combinedAdjustments, property: "effect/adjustments/saturation", value: -0.3) let effects = try engine.block.getEffects(secondImageBlock) print("Effects on second image: \(effects.count)") ``` By combining different adjustment properties, you can create warm and vibrant looks, cool and desaturated styles, or high-contrast dramatic effects. ## Refinement Adjustments Beyond basic color corrections, CE.SDK provides refinement adjustments for fine-tuning image detail and tonal balance. ```swift highlight-colorsAdjust-refinementAdjustments // Refinement properties target image detail and tonal balance rather than // global color shifts. let tempBlock = try engine.block.create(.graphic) try engine.block.setShape(tempBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(tempBlock, value: 150) try engine.block.setHeight(tempBlock, value: 100) try engine.block.setPositionX(tempBlock, value: 550) try engine.block.setPositionY(tempBlock, value: 50) try engine.block.appendChild(to: page, child: tempBlock) let tempFill = try engine.block.createFill(.image) try engine.block.setURL(tempFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(tempBlock, fill: tempFill) let refinementEffect = try engine.block.createEffect(.adjustments) try engine.block.appendEffect(tempBlock, effectID: refinementEffect) try engine.block.setFloat(refinementEffect, property: "effect/adjustments/sharpness", value: 0.4) try engine.block.setFloat(refinementEffect, property: "effect/adjustments/clarity", value: 0.35) try engine.block.setFloat(refinementEffect, property: "effect/adjustments/highlights", value: -0.2) try engine.block.setFloat(refinementEffect, property: "effect/adjustments/shadows", value: 0.3) ``` Refinement properties include: - **Sharpness** — Enhances edge definition for crisper details - **Clarity** — Increases mid-tone contrast for more depth and definition - **Highlights** — Controls the intensity of bright areas - **Shadows** — Controls the intensity of dark areas These adjustments are particularly useful for enhancing photos or preparing images for print. ## Remove Adjustments When you no longer need adjustments, remove them from the effect stack and free their resources. Always call `destroy` on effects that are no longer in use to prevent memory leaks. ```swift highlight-colorsAdjust-removeAdjustments // Remove an effect by its index in the stack, then destroy the returned // effect block to free its resources. let tempEffects = try engine.block.getEffects(tempBlock) if let effectIndex = tempEffects.firstIndex(of: refinementEffect) { try engine.block.removeEffect(tempBlock, index: effectIndex) } try engine.block.destroy(refinementEffect) ``` The `removeEffect` method takes an index position. After removal, destroy the effect instance to ensure proper cleanup. To reset all adjustments to their defaults, either set each property to `0.0` with `setFloat`, or remove the adjustments effect and create a new one. Setting properties to `0.0` is typically more efficient. ## Troubleshooting ### Adjustments Not Visible If adjustments don't appear after applying them: - Verify the block supports effects using `supportsEffects` - Check that the effect is enabled with `isEffectEnabled` - Ensure the adjustments effect was appended to the block, not just created - Confirm adjustment values are non-zero ### Unexpected Results If adjustments produce unexpected visual results: - Check the effect stack order—adjustments applied before or after other effects may produce different results - Verify property paths include the `effect/adjustments/` prefix - Use `findAllProperties` to verify correct property names ### Property Not Found If you encounter property not found errors: - Use `findAllProperties` to list every available property - Ensure property paths use the correct `effect/adjustments/` prefix format ## API Reference | Method | Description | |--------|-------------| | `block.supportsEffects(_:)` | Check if a block supports effects | | `block.createEffect(.adjustments)` | Create an adjustments effect | | `block.appendEffect(_:effectID:)` | Add effect to the end of the effect stack | | `block.insertEffect(_:effectID:index:)` | Insert effect at a specific position | | `block.getEffects(_:)` | Get all effects applied to a block | | `block.removeEffect(_:index:)` | Remove effect at the specified index | | `block.setEffectEnabled(effectID:enabled:)` | Enable or disable an effect | | `block.isEffectEnabled(effectID:)` | Check if an effect is enabled | | `block.setFloat(_:property:value:)` | Set a float property value | | `block.getFloat(_:property:)` | Get a float property value | | `block.findAllProperties(_:)` | List all properties of an effect | | `block.destroy(_:)` | Destroy an effect and free resources | ## Next Steps - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) — Apply colors to fills, strokes, and shadows - [Apply a Filter or Effect](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) — Apply LUT filters, duotone effects, and more - [Color Conversion](https://img.ly/docs/cesdk/mac-catalyst/colors/conversion-bcd82b/) — Convert between color spaces --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Apply Colors" description: "Apply solid colors to design elements using CE.SDK's sRGB, CMYK, and spot color system." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) --- Apply solid colors to design elements like shapes, text, and backgrounds using CE.SDK's color system with support for RGB, CMYK, and spot colors. ![A rectangular block with a blue fill, red stroke, and pink drop shadow](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-colors-apply) Colors in CE.SDK are applied to block properties like fill, stroke, and shadow using `engine.block.setColor`. The engine supports three color spaces: sRGB for screen display, CMYK for print production, and spot colors for specialized printing requirements. ```swift file=@cesdk_swift_examples/engine-guides-colors-apply/ApplyColors.swift reference-only import IMGLYEngine @MainActor func applyColors(engine: Engine) async throws { // Demo scaffolding: a scene with a page and a single graphic block to recolor. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.setWidth(block, value: 400) try engine.block.setHeight(block, value: 300) try engine.block.setPositionX(block, value: 200) try engine.block.setPositionY(block, value: 150) try engine.block.appendChild(to: page, child: block) let rgbaBlue = Color.rgba(r: 0.0, g: 0.0, b: 1.0, a: 1.0) let cmykRed = Color.cmyk(c: 0.0, m: 1.0, y: 1.0, k: 0.0, tint: 1.0) let spotPink = Color.spot(name: "Pink-Flamingo", tint: 1.0, externalReference: "Brand-Colors") engine.editor.setSpotColor(name: "Pink-Flamingo", r: 1.0, g: 0.41, b: 0.71) engine.editor.setSpotColor(name: "Corporate-Blue", c: 1.0, m: 0.5, y: 0.0, k: 0.2) let fill = try engine.block.getFill(block) try engine.block.setColor(fill, property: "fill/color/value", color: rgbaBlue) try await engine.captureGuide(page, label: "after-fill") let currentFillColor: Color = try engine.block.getColor(fill, property: "fill/color/value") print("Current fill color: \(currentFillColor)") try engine.block.setStrokeEnabled(block, enabled: true) try engine.block.setStrokeWidth(block, width: 4) try engine.block.setColor(block, property: "stroke/color", color: cmykRed) try await engine.captureGuide(page, label: "after-stroke") try engine.block.setDropShadowEnabled(block, enabled: true) try engine.block.setDropShadowOffsetX(block, offsetX: 5) try engine.block.setDropShadowOffsetY(block, offsetY: 5) try engine.block.setColor(block, property: "dropShadow/color", color: spotPink) try await engine.captureGuide(page, label: "hero") try await engine.editor.loadColorProfiles() let cmykFromRgb = try engine.editor.convertColorToColorSpace(color: rgbaBlue, colorSpace: .cmyk) let rgbFromCmyk = try engine.editor.convertColorToColorSpace(color: cmykRed, colorSpace: .sRGB) print("CMYK from RGB: \(cmykFromRgb)") print("RGB from CMYK: \(rgbFromCmyk)") let allSpotColors = engine.editor.findAllSpotColors() print("Defined spot colors: \(allSpotColors)") engine.editor.setSpotColor(name: "Pink-Flamingo", r: 1.0, g: 0.6, b: 0.8) try engine.editor.removeSpotColor(name: "Corporate-Blue") } ``` This guide covers how to create color values in different color spaces, apply colors to fill, stroke, and shadow properties, work with spot colors including defining and managing them, and convert colors between color spaces. ## Create Color Objects CE.SDK represents colors as a single `Color` enum with three cases — one per color space. Pick the case that matches your target output: `.rgba` for screens, `.cmyk` for print, or `.spot` for precise color matching. ```swift highlight-applyColors-createColors let rgbaBlue = Color.rgba(r: 0.0, g: 0.0, b: 1.0, a: 1.0) let cmykRed = Color.cmyk(c: 0.0, m: 1.0, y: 1.0, k: 0.0, tint: 1.0) let spotPink = Color.spot(name: "Pink-Flamingo", tint: 1.0, externalReference: "Brand-Colors") ``` RGB colors use `r`, `g`, `b`, and `a` (alpha) values from `0.0` to `1.0`. CMYK colors take `c`, `m`, `y`, `k`, and a `tint` that controls overall intensity. Spot colors reference a definition by `name`, with optional `tint` and `externalReference` fields. ## Define Spot Colors Before applying a spot color, define its screen preview approximation. The engine needs to know how to display the color since spot colors represent inks that can't be directly rendered on screens. ```swift highlight-applyColors-defineSpot engine.editor.setSpotColor(name: "Pink-Flamingo", r: 1.0, g: 0.41, b: 0.71) engine.editor.setSpotColor(name: "Corporate-Blue", c: 1.0, m: 0.5, y: 0.0, k: 0.2) ``` `engine.editor.setSpotColor(name:r:g:b:)` registers the RGB approximation; `engine.editor.setSpotColor(name:c:m:y:k:)` registers the CMYK approximation. A spot color can have both approximations defined at once. Neither method throws — they create the spot color if it doesn't yet exist or update it in place. ## Apply Fill Colors To set a block's fill color, first get the fill block using `engine.block.getFill`, then apply the color using `engine.block.setColor` with the `"fill/color/value"` property path. ```swift highlight-applyColors-applyFill let fill = try engine.block.getFill(block) try engine.block.setColor(fill, property: "fill/color/value", color: rgbaBlue) ``` The fill is a separate block from the design block. Color properties live on the fill, not on the parent — applying `"fill/color/value"` to the parent throws. ## Read a Block's Current Color Retrieve a block's current color using `engine.block.getColor`. The return type is `Color` — the same enum used when setting — so a single value carries its color space along with its components. ```swift highlight-applyColors-readColor let currentFillColor: Color = try engine.block.getColor(fill, property: "fill/color/value") print("Current fill color: \(currentFillColor)") ``` Swift's overload resolution can't pick between the deprecated `RGBA`-returning overload and the canonical `Color` one without help, so annotate the binding (`let currentFillColor: Color = ...`). ## Apply Stroke Colors Stroke colors are applied directly to the design block using the `"stroke/color"` property. Enable the stroke first with `engine.block.setStrokeEnabled`; without it, the color is set but nothing visible renders. ```swift highlight-applyColors-applyStroke try engine.block.setStrokeEnabled(block, enabled: true) try engine.block.setStrokeWidth(block, width: 4) try engine.block.setColor(block, property: "stroke/color", color: cmykRed) ``` The stroke renders around the edges of the block in the specified color. Use `engine.block.setStrokeWidth` to control the line thickness. ## Apply Shadow Colors Drop shadow colors use the `"dropShadow/color"` property on the design block. Enable shadows first using `engine.block.setDropShadowEnabled`. ```swift highlight-applyColors-applyShadow try engine.block.setDropShadowEnabled(block, enabled: true) try engine.block.setDropShadowOffsetX(block, offsetX: 5) try engine.block.setDropShadowOffsetY(block, offsetY: 5) try engine.block.setColor(block, property: "dropShadow/color", color: spotPink) ``` Control the shadow position with `setDropShadowOffsetX` and `setDropShadowOffsetY`. Spot colors work with shadows just like RGB or CMYK colors. ## Convert Between Color Spaces Use `engine.editor.convertColorToColorSpace` to convert any color to a different color space. The target `ColorSpace` enum has `.sRGB`, `.cmyk`, and `.spotColor` cases. ```swift highlight-applyColors-convertColor try await engine.editor.loadColorProfiles() let cmykFromRgb = try engine.editor.convertColorToColorSpace(color: rgbaBlue, colorSpace: .cmyk) let rgbFromCmyk = try engine.editor.convertColorToColorSpace(color: cmykRed, colorSpace: .sRGB) print("CMYK from RGB: \(cmykFromRgb)") print("RGB from CMYK: \(rgbFromCmyk)") ``` Spot colors convert to their defined approximation in the target space. Color conversions are approximations — CMYK has a smaller gamut than sRGB, so vibrant colors may appear muted after conversion. ## List Defined Spot Colors Query every spot color currently defined in the editor using `engine.editor.findAllSpotColors`. The method returns the spot color names as an array of strings. ```swift highlight-applyColors-listSpot let allSpotColors = engine.editor.findAllSpotColors() print("Defined spot colors: \(allSpotColors)") ``` This is useful for building color pickers or validating that required spot colors are defined before export. ## Update Spot Color Definitions Redefine a spot color's approximation by calling `setSpotColor` again with the same name. All blocks referencing that spot color automatically update their rendered appearance. ```swift highlight-applyColors-updateSpot engine.editor.setSpotColor(name: "Pink-Flamingo", r: 1.0, g: 0.6, b: 0.8) ``` This lets you adjust how a spot color appears on screen without touching every block that uses it. ## Remove Spot Color Definitions Remove a spot color definition using `engine.editor.removeSpotColor`. Blocks still referencing that color fall back to the default magenta approximation, which is a visual cue that something is missing. ```swift highlight-applyColors-removeSpot try engine.editor.removeSpotColor(name: "Corporate-Blue") ``` This is useful when cleaning up unused spot colors or signaling that a spot color is no longer valid. ## Troubleshooting ### Spot Color Appears Magenta The spot color wasn't defined before use. Call `setSpotColor(name:r:g:b:)` or `setSpotColor(name:c:m:y:k:)` with the exact spot color name before applying it to a block. ### Stroke or Shadow Color Not Visible The effect isn't enabled. Call `setStrokeEnabled(_:enabled:)` or `setDropShadowEnabled(_:enabled:)` before setting the color. ### Color Looks Different After Conversion Color space conversions are approximations. CMYK has a smaller gamut than sRGB, so vibrant colors may appear muted after conversion. ### Can't Apply Color to Fill Apply colors to the fill block returned from `getFill`, not the parent design block. The fill is a separate block with its own `"fill/color/value"` property. ## API Reference | Method | Description | |--------|-------------| | `engine.block.setColor(_:property:color:)` | Set a color property on a block | | `engine.block.getColor(_:property:)` | Get a color property from a block (annotate return as `Color`) | | `engine.block.getFill(_:)` | Get the fill block of a design block | | `engine.block.setStrokeEnabled(_:enabled:)` | Enable or disable stroke on a block | | `engine.block.setStrokeWidth(_:width:)` | Set stroke thickness | | `engine.block.setDropShadowEnabled(_:enabled:)` | Enable or disable drop shadow on a block | | `engine.editor.setSpotColor(name:r:g:b:)` | Define a spot color with RGB approximation | | `engine.editor.setSpotColor(name:c:m:y:k:)` | Define a spot color with CMYK approximation | | `engine.editor.findAllSpotColors()` | List every defined spot color | | `engine.editor.removeSpotColor(name:)` | Remove a spot color definition | | `engine.editor.convertColorToColorSpace(color:colorSpace:)` | Convert a color to a different color space | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Color Basics" description: "Learn how color works in CE.SDK, including the three supported color spaces (sRGB, CMYK, and Spot) and when to use each for screen display or print workflows." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/basics-307115/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [Color Basics](https://img.ly/docs/cesdk/mac-catalyst/colors/basics-307115/) --- ```swift file=@cesdk_swift_examples/engine-guides-colors-basics/ColorsBasics.swift reference-only import Foundation import IMGLYEngine @MainActor func colorsBasics(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Create a graphic block with a color fill let srgbBlock = try engine.block.create(.graphic) try engine.block.setShape(srgbBlock, shape: engine.block.createShape(.rect)) let srgbFill = try engine.block.createFill(.color) try engine.block.setFill(srgbBlock, fill: srgbFill) try engine.block.appendChild(to: page, child: srgbBlock) // Set fill color using an sRGB color (values 0.0-1.0) let srgbColor = Color.rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0) try engine.block.setColor(srgbFill, property: "fill/color/value", color: srgbColor) // Create another block with a CMYK color let cmykBlock = try engine.block.create(.graphic) try engine.block.setShape(cmykBlock, shape: engine.block.createShape(.rect)) let cmykFill = try engine.block.createFill(.color) try engine.block.setFill(cmykBlock, fill: cmykFill) try engine.block.appendChild(to: page, child: cmykBlock) // Set fill color using a CMYK color (values 0.0-1.0, tint scales ink) let cmykColor = Color.cmyk(c: 0.0, m: 0.8, y: 0.95, k: 0.0, tint: 1.0) try engine.block.setColor(cmykFill, property: "fill/color/value", color: cmykColor) // Define a spot color with an RGB approximation for screen preview engine.editor.setSpotColor(name: "MyBrand Red", r: 0.95, g: 0.25, b: 0.21) // You can also define a spot color with a CMYK approximation engine.editor.setSpotColor(name: "MyBrand Blue", c: 1.0, m: 0.7, y: 0.0, k: 0.1) // Create a block and apply the defined spot color let spotBlock = try engine.block.create(.graphic) try engine.block.setShape(spotBlock, shape: engine.block.createShape(.rect)) let spotFill = try engine.block.createFill(.color) try engine.block.setFill(spotBlock, fill: spotFill) try engine.block.appendChild(to: page, child: spotBlock) // Reference the spot color by name, with a tint and optional external reference let spotColor = Color.spot(name: "MyBrand Red", tint: 1.0, externalReference: "") try engine.block.setColor(spotFill, property: "fill/color/value", color: spotColor) // Enable stroke and apply a stroke color using sRGB try engine.block.setStrokeEnabled(srgbBlock, enabled: true) try engine.block.setStrokeWidth(srgbBlock, width: 4) try engine.block.setColor(srgbBlock, property: "stroke/color", color: .rgba(r: 0.1, g: 0.2, b: 0.5, a: 1.0)) // Apply a CMYK stroke color try engine.block.setStrokeEnabled(cmykBlock, enabled: true) try engine.block.setStrokeWidth(cmykBlock, width: 4) let cmykStroke = Color.cmyk(c: 0.0, m: 0.5, y: 0.6, k: 0.2, tint: 1.0) try engine.block.setColor(cmykBlock, property: "stroke/color", color: cmykStroke) // Apply a spot color stroke with reduced tint try engine.block.setStrokeEnabled(spotBlock, enabled: true) try engine.block.setStrokeWidth(spotBlock, width: 4) try engine.block.setColor(spotBlock, property: "stroke/color", color: .spot(name: "MyBrand Red", tint: 0.7)) // Read back color values from a property let readSrgb: Color = try engine.block.getColor(srgbFill, property: "fill/color/value") let readCmyk: Color = try engine.block.getColor(cmykFill, property: "fill/color/value") let readSpot: Color = try engine.block.getColor(spotFill, property: "fill/color/value") // The returned Color enum indicates the color space through its case for color in [readSrgb, readCmyk, readSpot] { switch color { case let .rgba(r, g, b, a): print("sRGB: r=\(r), g=\(g), b=\(b), a=\(a)") case let .cmyk(c, m, y, k, tint): print("CMYK: c=\(c), m=\(m), y=\(y), k=\(k), tint=\(tint)") case let .spot(name, tint, externalReference): print("Spot: name=\(name), tint=\(tint), ref=\(externalReference)") @unknown default: print("Unknown color space") } } try await engine.editor.loadColorProfiles() let swatches: [(label: String, color: Color)] = [ ("CMYK fill", cmykColor), ("CMYK stroke", cmykStroke), ("Spot MyBrand Blue", .spot(name: "MyBrand Blue", tint: 1.0)), ] func logConversions(_ step: String) throws { print(step) for swatch in swatches { let converted = try engine.editor.convertColorToColorSpace(color: swatch.color, colorSpace: .sRGB) print("\(swatch.label): \(converted)") } } try logConversions("Bundled default profile") let profileURL = URL(string: "bundle://ly.img.cesdk/colorprofiles/CGATS001Compat-v2-micro.icc")! let defaultCMYK = ProfileSlot(role: .default, space: .cmyk) try await engine.scene.setDocumentColorProfile(defaultCMYK, from: profileURL) let profileInfo = try engine.scene.getDocumentColorProfileInfo(defaultCMYK) print("Document profile hash: \(profileInfo.contentHash)") try logConversions("Document profile") try engine.scene.setColorRenderingIntent(.perceptual) engine.scene.setBlackPointCompensationEnabled(false) try logConversions("Perceptual intent, black point compensation off") let mode = try engine.block.getEnum(scene, property: "scene/colorConversionMode") print("Color conversion mode: \(mode)") try engine.block.setEnum(scene, property: "scene/colorConversionMode", value: "Legacy") try logConversions("Legacy mode") try engine.block.setEnum(scene, property: "scene/colorConversionMode", value: "Managed") } ``` Understand the three color spaces in CE.SDK and when to use each for screen or print workflows. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-colors-basics) CE.SDK supports three color spaces: **sRGB** for screen display, **CMYK** for print workflows, and **Spot Color** for specialized printing. Each color space is represented as a case of the Swift `Color` enum, and all three work with the unified `setColor()` / `getColor()` API. This guide covers how to choose the correct color space, define and apply colors using the `Color` enum, and configure spot colors with screen preview approximations. It also shows how to set the document CMYK profile, the rendering intent and black point compensation, and what each export format writes. ## Color Spaces Overview CE.SDK represents colors as cases of the `Color` enum: - `Color.rgba(r:g:b:a:)` — sRGB color for screen display - `Color.cmyk(c:m:y:k:tint:)` — CMYK color for print - `Color.spot(name:tint:externalReference:)` — Named spot color for specialized printing Use `engine.block.setColor(_:property:color:)` to apply any color type to supported properties. **Supported color properties:** - `'fill/color/value'` — Fill color of a block - `'stroke/color'` — Stroke/outline color - `'dropShadow/color'` — Drop shadow color - `'backgroundColor/color'` — Background color - `'camera/clearColor'` — Canvas clear color ## sRGB Colors sRGB is the default color space for screen display. Create a `Color.rgba` value with `r`, `g`, `b`, `a` components, each in the range 0.0 to 1.0. The `a` (alpha) component controls transparency. ```swift highlight-colorsBasics-srgb // Create a graphic block with a color fill let srgbBlock = try engine.block.create(.graphic) try engine.block.setShape(srgbBlock, shape: engine.block.createShape(.rect)) let srgbFill = try engine.block.createFill(.color) try engine.block.setFill(srgbBlock, fill: srgbFill) try engine.block.appendChild(to: page, child: srgbBlock) // Set fill color using an sRGB color (values 0.0-1.0) let srgbColor = Color.rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0) try engine.block.setColor(srgbFill, property: "fill/color/value", color: srgbColor) ``` sRGB colors are ideal for web and digital content where the output is displayed on screens. ## CMYK Colors CMYK is the color space for print workflows. Create a `Color.cmyk` value with `c`, `m`, `y`, `k` components (0.0 to 1.0) plus a `tint` value that scales the ink amounts (1.0 is full ink, 0.0 is no ink). ```swift highlight-colorsBasics-cmyk // Create another block with a CMYK color let cmykBlock = try engine.block.create(.graphic) try engine.block.setShape(cmykBlock, shape: engine.block.createShape(.rect)) let cmykFill = try engine.block.createFill(.color) try engine.block.setFill(cmykBlock, fill: cmykFill) try engine.block.appendChild(to: page, child: cmykBlock) // Set fill color using a CMYK color (values 0.0-1.0, tint scales ink) let cmykColor = Color.cmyk(c: 0.0, m: 0.8, y: 0.95, k: 0.0, tint: 1.0) try engine.block.setColor(cmykFill, property: "fill/color/value", color: cmykColor) ``` On screen and in raster exports, CMYK colors convert to RGB through the document CMYK profile. A key-only black and a rich black look different, as they do on press. The `tint` value (0.0 to 1.0) scales the ink amounts before the conversion, so a lower tint gives a lighter color that stays opaque. A scene saved before CE.SDK 1.83 loads as `Legacy` and keeps the earlier formula. The section "Managed and Legacy Scenes" below explains both modes. > **Note:** PDF export converts CMYK colors to RGB. In a `Managed` scene, it converts them > through the document CMYK profile. Tint lightens the color and does not add > transparency. ## Spot Colors Spot colors are named colors used for specialized printing. Before using a spot color, you must define it with an RGB or CMYK approximation for screen preview. ### Defining Spot Colors Use `engine.editor.setSpotColor(name:r:g:b:)` or `engine.editor.setSpotColor(name:c:m:y:k:)` to register a spot color with its screen preview approximation. ```swift highlight-colorsBasics-defineSpot // Define a spot color with an RGB approximation for screen preview engine.editor.setSpotColor(name: "MyBrand Red", r: 0.95, g: 0.25, b: 0.21) // You can also define a spot color with a CMYK approximation engine.editor.setSpotColor(name: "MyBrand Blue", c: 1.0, m: 0.7, y: 0.0, k: 0.1) ``` ### Applying Spot Colors Reference a defined spot color using `Color.spot(name:tint:externalReference:)`. The `tint` lightens the color on screen. It does not add transparency. ```swift highlight-colorsBasics-spot // Create a block and apply the defined spot color let spotBlock = try engine.block.create(.graphic) try engine.block.setShape(spotBlock, shape: engine.block.createShape(.rect)) let spotFill = try engine.block.createFill(.color) try engine.block.setFill(spotBlock, fill: spotFill) try engine.block.appendChild(to: page, child: spotBlock) // Reference the spot color by name, with a tint and optional external reference let spotColor = Color.spot(name: "MyBrand Red", tint: 1.0, externalReference: "") try engine.block.setColor(spotFill, property: "fill/color/value", color: spotColor) ``` When rendered on screen, the spot color uses its RGB approximation. It uses the CMYK approximation only when no RGB approximation is set. During PDF export, spot colors are saved as a [Separation Color Space](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.6.pdf#G9.1850648) that preserves print information. > **Note:** If a block references an undefined spot color, CE.SDK displays magenta (RGB: > 1, 0, 1) as a fallback. ## Applying Stroke Colors Strokes support all three color spaces. Enable the stroke, set its width, then apply a color using the `'stroke/color'` property. ```swift highlight-colorsBasics-stroke // Enable stroke and apply a stroke color using sRGB try engine.block.setStrokeEnabled(srgbBlock, enabled: true) try engine.block.setStrokeWidth(srgbBlock, width: 4) try engine.block.setColor(srgbBlock, property: "stroke/color", color: .rgba(r: 0.1, g: 0.2, b: 0.5, a: 1.0)) // Apply a CMYK stroke color try engine.block.setStrokeEnabled(cmykBlock, enabled: true) try engine.block.setStrokeWidth(cmykBlock, width: 4) let cmykStroke = Color.cmyk(c: 0.0, m: 0.5, y: 0.6, k: 0.2, tint: 1.0) try engine.block.setColor(cmykBlock, property: "stroke/color", color: cmykStroke) // Apply a spot color stroke with reduced tint try engine.block.setStrokeEnabled(spotBlock, enabled: true) try engine.block.setStrokeWidth(spotBlock, width: 4) try engine.block.setColor(spotBlock, property: "stroke/color", color: .spot(name: "MyBrand Red", tint: 0.7)) ``` ## Reading Color Values Use `engine.block.getColor(_:property:)` to retrieve the current color value from a property. The returned `Color` enum indicates the color space through its case. ```swift highlight-colorsBasics-getColor // Read back color values from a property let readSrgb: Color = try engine.block.getColor(srgbFill, property: "fill/color/value") let readCmyk: Color = try engine.block.getColor(cmykFill, property: "fill/color/value") let readSpot: Color = try engine.block.getColor(spotFill, property: "fill/color/value") // The returned Color enum indicates the color space through its case for color in [readSrgb, readCmyk, readSpot] { switch color { case let .rgba(r, g, b, a): print("sRGB: r=\(r), g=\(g), b=\(b), a=\(a)") case let .cmyk(c, m, y, k, tint): print("CMYK: c=\(c), m=\(m), y=\(y), k=\(k), tint=\(tint)") case let .spot(name, tint, externalReference): print("Spot: name=\(name), tint=\(tint), ref=\(externalReference)") @unknown default: print("Unknown color space") } } ``` ## How CE.SDK Converts Colors To show a CMYK color on screen, CE.SDK converts it through an ICC profile, the document CMYK profile. The engine converts a color in these steps: - **The document CMYK profile.** Each scene can name one CMYK ICC profile. The profile converts CMYK colors, the CMYK approximation of a spot color, and CMYK images that have no profile of their own. - **Profile priority.** The engine uses the document profile when the document names one. Otherwise, it uses the profile in the `fallbackCMYKProfileUri` setting. - **A profile that does not load.** When a loaded scene names a profile that does not load, the engine logs a warning and uses the fallback profile. When the fallback profile does not load, the engine does not use the bundled profile instead. CMYK colors then show with the unmanaged formula, and `loadColorProfiles()` and a conversion that needs the profile fail with `COLOR.PROFILE_MISSING`. - **Rendering intent and black point compensation.** Both apply to every conversion through the document profile. The defaults are relative colorimetric and black point compensation on. - **What changes on screen.** A key-only black and a rich black are two different blacks, as they are on press. A tinted CMYK color scales its inks. It does not fade toward white. - **Images.** An image with an embedded ICC profile, RGB or CMYK, uses its own profile. The document profile does not change it. A CMYK or YCCK JPEG without a profile decodes through the document CMYK profile. ## Converting Colors in Code `engine.editor.convertColorToColorSpace(color:colorSpace:)` with `.sRGB` or `.cmyk` converts through the same profile as the canvas, so a swatch that you show matches what the user sees. The call is synchronous. You cannot convert to `.spotColor`. A scene that you create with `engine.scene.create()` is `Managed`, which means that it converts through the profile. Applying a template or loading a scene takes the mode stored in that file. A template or scene saved before CE.SDK 1.83 is `Legacy`, and this includes the templates of the demo asset library. The section "Managed and Legacy Scenes" below explains the other mode. In a `Managed` scene, a conversion that needs the profile fails with `COLOR.PROFILE_NOT_LOADED` while the profile loads. The call starts the load. Await `engine.editor.loadColorProfiles()` before you convert. Await it again after each change to the profile source: `removeDocumentColorProfile(defaultCMYK)`, a change of `fallbackCMYKProfileUri`, or a scene load. The example awaits the profile and defines a `logConversions` helper, which logs the sRGB value of each swatch. The swatches are the CMYK fill, the CMYK stroke and the spot color `MyBrand Blue`. The later steps call the helper again. ```swift highlight-colorsBasics-convertColors try await engine.editor.loadColorProfiles() let swatches: [(label: String, color: Color)] = [ ("CMYK fill", cmykColor), ("CMYK stroke", cmykStroke), ("Spot MyBrand Blue", .spot(name: "MyBrand Blue", tint: 1.0)), ] func logConversions(_ step: String) throws { print(step) for swatch in swatches { let converted = try engine.editor.convertColorToColorSpace(color: swatch.color, colorSpace: .sRGB) print("\(swatch.label): \(converted)") } } try logConversions("Bundled default profile") ``` - sRGB to CMYK uses the reverse tables of the profile. A profile that has no reverse tables uses a simple formula. - A spot color converts to CMYK through its CMYK approximation, when it has one. The logged values change when the profile, the intent, black point compensation or the mode changes. The [Color Conversion](https://img.ly/docs/cesdk/mac-catalyst/colors/conversion-bcd82b/) guide has more conversion examples. The loader currently waits only for the effective Default/CMYK profile, including fallback resolution. It does not load other document profile slots. ## Setting the Document CMYK Profile Set the document CMYK profile to the profile of your press and paper. Use the document color profile methods with the Default/CMYK slot. In the example, `defaultCMYK` is `ProfileSlot(role: .default, space: .cmyk)`. - `engine.scene.setDocumentColorProfile(defaultCMYK, from:)` with a `URL` loads a profile from a URL. The call is async and atomic: the previous profile stays in effect while the new one loads, and a failure keeps it. - `engine.scene.setDocumentColorProfile(defaultCMYK, from:)` with `Data` sets the profile from bytes that you already have. The change is in effect when the call returns. - `engine.scene.getDocumentColorProfileInfo(defaultCMYK)` returns the `contentHash` of the profile that the document names, also when that profile did not load. To learn if the profile is usable, await `engine.editor.loadColorProfiles()`. The getter fails with `COLOR.DOCUMENT_PROFILE_NOT_SET` when the document names no profile. It never reports the fallback profile. - `engine.scene.removeDocumentColorProfile(defaultCMYK)` goes back to the fallback profile. While `fallbackCMYKProfileUri` is not set, that is the bundled profile. Color management stays on. - The `fallbackCMYKProfileUri` setting changes the profile for every document that names none. Set it with `engine.editor.setSettingString("fallbackCMYKProfileUri", value: profileURI)`. The example sets the bundled profile as the document profile and logs the content hash. The bundled profile is also the fallback profile, so the logged values stay the same. Replace the URL with the profile of your press and paper. `try await engine.scene.setDocumentColorProfile(defaultCMYK, from:)` waits for the new profile, so the example converts right after it. ```swift highlight-colorsBasics-setProfile let profileURL = URL(string: "bundle://ly.img.cesdk/colorprofiles/CGATS001Compat-v2-micro.icc")! let defaultCMYK = ProfileSlot(role: .default, space: .cmyk) try await engine.scene.setDocumentColorProfile(defaultCMYK, from: profileURL) let profileInfo = try engine.scene.getDocumentColorProfileInfo(defaultCMYK) print("Document profile hash: \(profileInfo.contentHash)") try logConversions("Document profile") ``` ## Rendering Intent and Black Point Compensation The rendering intent selects how the profile maps colors into the destination, for example from the press to the screen: - `.perceptual` compresses the full gamut, so the relations between colors stay the same. - `.relativeColorimetric`, the default, keeps the colors inside the gamut and clips the others. It maps the paper white of the profile to the white of the screen. - `.saturation` keeps colors saturated. Use it for charts and graphics, not for photos. - `.absoluteColorimetric` does not map the paper white to the white of the screen. The screen shows the paper tint of the profile, so the preview simulates the paper color. Black point compensation maps the darkest black of the profile to the darkest black of the screen. It is on by default. With the relative colorimetric intent, turning it off leaves the press black as a dark gray, so dark areas look lighter. The absolute colorimetric intent does not use black point compensation. The example sets the `.perceptual` intent, turns black point compensation off and logs the conversions again. ```swift highlight-colorsBasics-intentBPC try engine.scene.setColorRenderingIntent(.perceptual) engine.scene.setBlackPointCompensationEnabled(false) try logConversions("Perceptual intent, black point compensation off") ``` Both settings are document state. CE.SDK saves them with the scene, and undo does not change them. ## Managed and Legacy Scenes The `scene/colorConversionMode` property selects how the scene converts CMYK colors. - A scene that you create with `engine.scene.create()` is `Managed` and converts through the profile. - Applying a template or loading a scene takes the mode stored in that file. A template or scene saved before CE.SDK 1.83 is `Legacy`, and this includes the templates of the demo asset library. - A `Legacy` scene keeps the old CMYK conversion, which inverts the ink channels and blends a tint toward white. It also keeps the old decode of untagged CMYK JPEGs. So its CMYK colors and untagged CMYK JPEGs look as they did when you saved them. - A `Legacy` scene never needs a profile and never fails a conversion for a missing profile. A profile that you set on a `Legacy` scene has no effect until the scene becomes `Managed`. - Spot colors use the corrected approximation and tint in both modes. A CMYK spot approximation follows the mode. So spot gradient stops and tinted spot drop shadows can look different from the saved design, also in a `Legacy` scene. Read the mode with `engine.block.getEnum(_:property:)`. ```swift highlight-colorsBasics-checkMode let mode = try engine.block.getEnum(scene, property: "scene/colorConversionMode") print("Color conversion mode: \(mode)") ``` To move a saved design to accurate colors, set the mode to `Managed` with `engine.block.setEnum(_:property:value:)`. Its CMYK colors then change on screen and in raster exports, so review the design after the switch. The example switches to `Legacy` to show the old values, then back to `Managed`. ```swift highlight-colorsBasics-legacyMode try engine.block.setEnum(scene, property: "scene/colorConversionMode", value: "Legacy") try logConversions("Legacy mode") try engine.block.setEnum(scene, property: "scene/colorConversionMode", value: "Managed") ``` ## What Each Export Writes | Output | sRGB colors | CMYK colors | Spot colors | Images | | --------------- | ----------- | ------------------------------------------------------ | ------------------------------------- | ------ | | PNG, JPEG, WebP | sRGB | Converted through the profile, the same as the preview | Approximation | RGB | | PDF | DeviceRGB | DeviceRGB, converted through the profile | Separation with a DeviceRGB alternate | RGB | - A `Legacy` scene uses the earlier formula for these outputs, not the profile. - Gradient stops are always converted to RGB. This includes CMYK and spot color stops. - The engine PDF embeds no OutputIntent and no ICC profile. - Overprint (`setFillOverprint(_:overprint:)`, `setStrokeOverprint(_:overprint:)`) applies to spot colors in a PDF only. ## Export for Print On this platform, the engine writes an RGB PDF. CMYK colors are written as RGB values, converted through the profile. Spot colors stay named Separations. A PDF with DeviceCMYK colors needs a Web or Node export. To prepare a design for print: 1. Set the document CMYK profile to your press condition, keep the scene `Managed`, and use CMYK or spot colors. 2. Export the PDF with `engine.block.export(_:mimeType:)` and `.pdf`. The engine PDF has no OutputIntent and keeps images in RGB. The [Export for Printing](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-printing-bca896/) guide shows the next steps. ## Troubleshooting The [Error Catalog](https://img.ly/docs/cesdk/mac-catalyst/concepts/error-catalog-z3djzn/) lists every `COLOR.*` error. | Symptom or error | Cause | Fix | | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | The CMYK colors of an old design look the same after the upgrade | It loaded as `Legacy` | Set `scene/colorConversionMode` to `Managed` | | The profile, intent or black point compensation has no effect after you apply a template | The template was saved before 1.83, so the scene is `Legacy` | Set `scene/colorConversionMode` to `Managed` | | Black areas look lighter than before | A key-only black now shows as it prints | Use a rich black, or keep the accurate preview | | `COLOR.PROFILE_NOT_LOADED` | A conversion ran before the profile loaded | Await `engine.editor.loadColorProfiles()` | | `COLOR.PROFILE_MISSING` from `setDocumentColorProfile(defaultCMYK, from:)` | The engine cannot load the URL | Check the URL. The previous profile stays in effect | | `COLOR.PROFILE_MISSING` from `loadColorProfiles()` or a conversion | The fallback profile does not load, so no profile is ready | Check the URI in the error and the `fallbackCMYKProfileUri` setting | | `COLOR.PROFILE_SPACE_MISMATCH`, `COLOR.PROFILE_DATA_SPACE_MISMATCH` | The profile is not a CMYK profile | Use a CMYK output profile | | `COLOR.PROFILE_INVALID`, `COLOR.PROFILE_UNSUPPORTED_SPACE` | The bytes are not a usable ICC profile | Check the file | | `COLOR.PROFILE_ASSIGNMENT_SUPERSEDED` | A later set, remove or scene load replaced a pending set | Expected. Await the latest call | | `COLOR.DOCUMENT_PROFILE_NOT_SET` | `getDocumentColorProfileInfo(defaultCMYK)` ran when the document names no profile | Set a profile, or treat it as "the document uses the fallback" | | `COLOR.IMAGE_DECODE_FAILED`, `COLOR.IMAGE_TRANSFORM_FAILED` | The engine cannot read a CMYK image or its profile | The image loads unmanaged. Replace the file | | A profile set from bytes is lost after a reload | The scene was saved with `saveToString()` | Save an archive | ## API Reference | Method | Description | | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `engine.block.setColor(_:property:color:)` | Set a color property on a block. Pass a `Color.rgba`, `Color.cmyk`, or `Color.spot` value. | | `engine.block.getColor(_:property:)` | Get the current color value from a property. Returns a `Color` enum value. | | `engine.editor.setSpotColor(name:r:g:b:)` | Define a spot color with an RGB approximation for screen preview. Components range from 0.0 to 1.0. | | `engine.editor.setSpotColor(name:c:m:y:k:)` | Define a spot color with a CMYK approximation for screen preview. Components range from 0.0 to 1.0. | | `engine.scene.setDocumentColorProfile(defaultCMYK, from:)` with a `URL` | Load the document CMYK profile from a URL. | | `engine.scene.setDocumentColorProfile(defaultCMYK, from:)` with `Data` | Set the document CMYK profile from bytes. | | `engine.scene.getDocumentColorProfileInfo(defaultCMYK)` | Read the content hash of the document profile. | | `engine.scene.removeDocumentColorProfile(defaultCMYK)` | Go back to the fallback profile. | | `engine.scene.getColorRenderingIntent()` | Read the rendering intent. | | `engine.scene.setColorRenderingIntent(_:)` | Set the rendering intent. | | `engine.scene.isBlackPointCompensationEnabled()` | Read black point compensation. | | `engine.scene.setBlackPointCompensationEnabled(_:)` | Turn black point compensation on or off. | | `engine.editor.loadColorProfiles()` | Wait until a CMYK profile is ready. | | `engine.editor.convertColorToColorSpace(color:colorSpace:)` | Convert a color through the document profile. | | `engine.editor.setSettingString("fallbackCMYKProfileUri", value:)` | Set the profile for documents that name none. | | `engine.block.getEnum(_:property:)` / `setEnum(_:property:value:)` with `scene/colorConversionMode` | Read or switch Managed and Legacy. | | Type | Properties | Description | | ------------ | ------------------------------------ | ----------------------------------------------------------- | | `Color.rgba` | `r`, `g`, `b`, `a` (0.0-1.0) | sRGB color for screen display. Alpha controls transparency. | | `Color.cmyk` | `c`, `m`, `y`, `k`, `tint` (0.0-1.0) | CMYK color for print. Tint scales the ink amounts. | | `Color.spot` | `name`, `tint`, `externalReference` | Named color for specialized printing. | ## Next Steps - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) — Apply colors to design elements programmatically - [CMYK Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/cmyk-8a1334/) — Work with CMYK for print workflows - [Spot Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/spot-c3a150/) — Define and manage spot colors for specialized printing - [Color Conversion](https://img.ly/docs/cesdk/mac-catalyst/colors/conversion-bcd82b/) - [Export for Printing](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-printing-bca896/) - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) - [Error Catalog](https://img.ly/docs/cesdk/mac-catalyst/concepts/error-catalog-z3djzn/) --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Color Conversion" description: "Convert colors between sRGB, CMYK, and spot color spaces programmatically in CE.SDK for Swift." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/conversion-bcd82b/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [Color Conversion](https://img.ly/docs/cesdk/mac-catalyst/colors/conversion-bcd82b/) --- ```swift file=@cesdk_swift_examples/engine-guides-color-conversion/ColorConversion.swift reference-only import Foundation import IMGLYEngine @MainActor func colorConversion(engine: Engine) async throws { engine.editor.setSpotColor(name: "Brand Red", r: 0.8, g: 0.1, b: 0.1) engine.editor.setSpotColor(name: "Brand Red", c: 0.0, m: 0.95, y: 0.95, k: 0.1) let cmykCyan = Color.cmyk(c: 1.0, m: 0.0, y: 0.0, k: 0.0, tint: 1.0) try await engine.editor.loadColorProfiles() let cyanAsSrgb = try engine.editor.convertColorToColorSpace(color: cmykCyan, colorSpace: .sRGB) print("CMYK cyan as sRGB: \(cyanAsSrgb)") let srgbRed = Color.rgba(r: 1.0, g: 0.0, b: 0.0, a: 1.0) let redAsCmyk = try engine.editor.convertColorToColorSpace(color: srgbRed, colorSpace: .cmyk) print("sRGB red as CMYK: \(redAsCmyk)") let spot = Color.spot(name: "Brand Red", tint: 1.0, externalReference: "") let spotAsSrgb = try engine.editor.convertColorToColorSpace(color: spot, colorSpace: .sRGB) let spotAsCmyk = try engine.editor.convertColorToColorSpace(color: spot, colorSpace: .cmyk) print("Spot 'Brand Red' as sRGB: \(spotAsSrgb)") print("Spot 'Brand Red' as CMYK: \(spotAsCmyk)") let unknown: Color = redAsCmyk switch unknown { case let .rgba(r, g, b, a): print("sRGB: r=\(r), g=\(g), b=\(b), a=\(a)") case let .cmyk(c, m, y, k, tint): print("CMYK: c=\(c), m=\(m), y=\(y), k=\(k), tint=\(tint)") case let .spot(name, tint, externalReference): print("Spot: name=\(name), tint=\(tint), ref=\(externalReference)") @unknown default: print("Unknown color space") } let space: ColorSpace = unknown.colorSpace print("Color space: \(space)") // Build display values for a custom color picker. The input would normally // come from `engine.block.getColor`; here a literal stands in for a real // block color. let pickerInput: Color = .cmyk(c: 0.5, m: 0.0, y: 1.0, k: 0.0, tint: 1.0) let pickerSrgb = try engine.editor.convertColorToColorSpace(color: pickerInput, colorSpace: .sRGB) if case let .rgba(r, g, b, _) = pickerSrgb { print("R: \(Int(r * 255)), G: \(Int(g * 255)), B: \(Int(b * 255))") } // Before PDF export, ensure each color is in CMYK. Skip conversion when it // already is. let exportInput: Color = .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0) if case .cmyk = exportInput { print("Already CMYK: \(exportInput)") } else { let cmyk = try engine.editor.convertColorToColorSpace(color: exportInput, colorSpace: .cmyk) print("Converted to CMYK: \(cmyk)") } } ``` Convert colors between sRGB, CMYK, and spot color spaces programmatically in CE.SDK. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-color-conversion) CE.SDK supports three color spaces: sRGB, CMYK, and Spot Color. When building color interfaces or preparing designs for export, you may need to convert colors between these spaces. The engine handles the mathematical conversion automatically through the `convertColorToColorSpace(color:colorSpace:)` API. This guide covers how to convert colors between sRGB and CMYK, handle spot color conversions, identify color types with enum pattern matching, and understand how tint and alpha affect conversion. ## Supported Color Spaces CE.SDK supports conversion between three color spaces. Each is represented as a case of the `Color` enum: | Color Space | Swift Case | Use Case | | -------------- | ------------------------------------------ | -------------------- | | **sRGB** | `Color.rgba(r:g:b:a:)` (0.0-1.0) | Screen display | | **CMYK** | `Color.cmyk(c:m:y:k:tint:)` (0.0-1.0) | Print workflows | | **Spot Color** | `Color.spot(name:tint:externalReference:)` | Specialized printing | The `ColorSpace` enum identifies the target of a conversion. Its cases are `.sRGB`, `.cmyk`, and `.spotColor`. ## Setting Up Colors Construct a color value directly with the appropriate `Color` enum case: - An sRGB color uses `Color.rgba(r:g:b:a:)`, where alpha controls transparency. - A CMYK color uses `Color.cmyk(c:m:y:k:tint:)`, where tint controls intensity. - A spot color uses `Color.spot(name:tint:externalReference:)` and references a name defined on the editor. Before converting a spot color, define its RGB or CMYK approximation. The approximation is also used when the spot color is rendered on screen. ```swift highlight-colorConversion-defineSpot engine.editor.setSpotColor(name: "Brand Red", r: 0.8, g: 0.1, b: 0.1) engine.editor.setSpotColor(name: "Brand Red", c: 0.0, m: 0.95, y: 0.95, k: 0.1) ``` You can call both overloads of `setSpotColor` for the same name so the spot color converts cleanly into either target space. ## Loading the CMYK Profile In Managed scenes, RGB-to-CMYK conversion uses the document CMYK profile, or the fallback profile when no document profile is available. The synchronous conversion reports `COLOR.PROFILE_NOT_LOADED` only while a profile from a URI loads. Await `loadColorProfiles()` before converting colors to avoid this loading error. Currently, it loads only the effective Default/CMYK profile, including fallback resolution: ```swift try await engine.editor.loadColorProfiles() ``` The loading call can fail if no usable profile is available. RGB-to-CMYK conversion uses a simple formula if the profile cannot load or only converts CMYK to RGB. Legacy scenes always use the simple formula for RGB-to-CMYK conversion. ## Converting to sRGB Use `engine.editor.convertColorToColorSpace(color:colorSpace:)` with `colorSpace: .sRGB` to convert any color to sRGB. The method throws if the conversion fails. ```swift highlight-colorConversion-toSrgb let cmykCyan = Color.cmyk(c: 1.0, m: 0.0, y: 0.0, k: 0.0, tint: 1.0) try await engine.editor.loadColorProfiles() let cyanAsSrgb = try engine.editor.convertColorToColorSpace(color: cmykCyan, colorSpace: .sRGB) print("CMYK cyan as sRGB: \(cyanAsSrgb)") ``` When converting CMYK or spot colors to sRGB, the engine returns a `Color.rgba` value. The result is opaque. In Managed scenes, CMYK tint scales the ink values before conversion. RGB spot tint blends toward white. ## Converting to CMYK Use `engine.editor.convertColorToColorSpace(color:colorSpace:)` with `colorSpace: .cmyk` to convert any color to CMYK. This is essential for print workflows where colors must be in the correct space before export. ```swift highlight-colorConversion-toCmyk let srgbRed = Color.rgba(r: 1.0, g: 0.0, b: 0.0, a: 1.0) let redAsCmyk = try engine.editor.convertColorToColorSpace(color: srgbRed, colorSpace: .cmyk) print("sRGB red as CMYK: \(redAsCmyk)") ``` RGB alpha does not generally become CMYK tint. Preserve transparency separately. Spot colors retain a registered CMYK approximation. In Managed scenes, a spot with only an RGB approximation converts through the CMYK profile with its tint applied. > **Note:** Color space conversions may not be perfectly reversible. Some sRGB colors > cannot be exactly represented in CMYK due to different color gamuts. Spot colors convert into both target spaces using the approximations you registered above: ```swift highlight-colorConversion-spotConvert let spot = Color.spot(name: "Brand Red", tint: 1.0, externalReference: "") let spotAsSrgb = try engine.editor.convertColorToColorSpace(color: spot, colorSpace: .sRGB) let spotAsCmyk = try engine.editor.convertColorToColorSpace(color: spot, colorSpace: .cmyk) print("Spot 'Brand Red' as sRGB: \(spotAsSrgb)") print("Spot 'Brand Red' as CMYK: \(spotAsCmyk)") ``` ## Identifying Color Types Before converting a color, you may need to know which color space it currently uses. Swift's enum pattern matching makes this straightforward — use a `switch` statement to handle each `Color` case, or check the `colorSpace` property when you only need the type. ```swift highlight-colorConversion-identify let unknown: Color = redAsCmyk switch unknown { case let .rgba(r, g, b, a): print("sRGB: r=\(r), g=\(g), b=\(b), a=\(a)") case let .cmyk(c, m, y, k, tint): print("CMYK: c=\(c), m=\(m), y=\(y), k=\(k), tint=\(tint)") case let .spot(name, tint, externalReference): print("Spot: name=\(name), tint=\(tint), ref=\(externalReference)") @unknown default: print("Unknown color space") } let space: ColorSpace = unknown.colorSpace print("Color space: \(space)") ``` The `colorSpace` property returns a `ColorSpace` enum value (`.sRGB`, `.cmyk`, or `.spotColor`) without unpacking the underlying components. ## Handling Tint and Alpha Alpha controls transparency. Tint controls color strength and does not generally preserve alpha: | Source | Target | Transformation | | ------------ | ------ | --------------------------------------------------------------------------------------------- | | sRGB (alpha) | CMYK | Alpha is not generally preserved; keep transparency separately | | CMYK (tint) | sRGB | Tint scales ink values; the result is opaque | | Spot (tint) | sRGB | Tint changes the approximation; the result is opaque | | Spot (tint) | CMYK | CMYK approximations retain tint; Managed RGB-only approximations apply tint before conversion | ## Practical Use Cases ### Building a Color Picker When displaying a color value retrieved from a block with `engine.block.getColor(_:property:)`, convert it to sRGB to show RGB components in a custom picker. Pattern-matching on the returned `.rgba` case unpacks the components for display. ```swift highlight-colorConversion-colorPicker // Build display values for a custom color picker. The input would normally // come from `engine.block.getColor`; here a literal stands in for a real // block color. let pickerInput: Color = .cmyk(c: 0.5, m: 0.0, y: 1.0, k: 0.0, tint: 1.0) let pickerSrgb = try engine.editor.convertColorToColorSpace(color: pickerInput, colorSpace: .sRGB) if case let .rgba(r, g, b, _) = pickerSrgb { print("R: \(Int(r * 255)), G: \(Int(g * 255)), B: \(Int(b * 255))") } ``` ### Export Preparation Before PDF export for print, check the current color space and convert to CMYK only when needed. Skipping the conversion for colors that are already CMYK avoids unnecessary gamut shifts. ```swift highlight-colorConversion-exportPrep // Before PDF export, ensure each color is in CMYK. Skip conversion when it // already is. let exportInput: Color = .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0) if case .cmyk = exportInput { print("Already CMYK: \(exportInput)") } else { let cmyk = try engine.editor.convertColorToColorSpace(color: exportInput, colorSpace: .cmyk) print("Converted to CMYK: \(cmyk)") } ``` ## Troubleshooting | Issue | Cause | Solution | | ---------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | | Spot color converts to unexpected values | Spot color not defined | Call `setSpotColor(name:r:g:b:)` or `setSpotColor(name:c:m:y:k:)` before conversion | | Colors look different after conversion | Color gamut differences | Some sRGB colors cannot be exactly represented in CMYK | | Mixing up `Color` cases | Direct property access on the wrong case | Use a `switch` statement or `if case` pattern matching to safely unpack components | ## API Reference | Method | Description | | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `engine.editor.convertColorToColorSpace(color:colorSpace:)` | Convert a color to the target color space. Returns a `Color` enum value matching the requested space. | | `engine.editor.setSpotColor(name:r:g:b:)` | Define a spot color with an RGB approximation. Components range from 0.0 to 1.0. | | `engine.editor.setSpotColor(name:c:m:y:k:)` | Define a spot color with a CMYK approximation. Components range from 0.0 to 1.0. | | Type | Description | | ------------------------------------------ | ----------------------------------------------------------- | | `Color.rgba(r:g:b:a:)` | sRGB color for screen display. Alpha controls transparency. | | `Color.cmyk(c:m:y:k:tint:)` | CMYK color for print. Tint controls ink strength. | | `Color.spot(name:tint:externalReference:)` | Named color for specialized printing. | | `ColorSpace` | Enum with cases `.sRGB`, `.cmyk`, and `.spotColor`. | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Extract Dominant Colors" description: "Read the most prominent colors from the rendered appearance of a block with the CE.SDK engine on Apple platforms." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/extract-colors-d4c0a1/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [Extract Dominant Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/extract-colors-d4c0a1/) --- Read the most prominent colors out of a block directly from the engine. The result reflects what is actually rendered on the canvas, so crops, color adjustments, and effects are all taken into account. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. `engine.block.getDominantColors(_:options:)` analyzes the rendered appearance of a block and returns its most prominent colors, sorted from most to least dominant. It works on any visible block — an image fill, a solid graphic, or a whole page — and runs entirely inside the engine. ## Extracting Dominant Colors Pass a block and await the result. By default you get up to five colors, ordered by how much of the rendered block they cover. ```swift let block = try engine.block.find(byType: .graphic).first! let colors = try await engine.block.getDominantColors(block) for color in colors { let percent = Int((color.weight * 100).rounded()) print("rgb(\(color.r), \(color.g), \(color.b)) covers \(percent)%") } ``` The method is asynchronous because the engine first resolves the block's final layout. If the block still has assets loading — for example an image fill whose source has not finished downloading — the call waits for them to settle instead of returning an empty result. ## Understanding the Result Each entry is a `DominantColor` describing one extracted color: | Property | Type | Description | | -------- | ------- | ---------------------------------------------------------------- | | `r` | `Float` | Red component in sRGB, normalized to `0`–`1`. | | `g` | `Float` | Green component in sRGB, normalized to `0`–`1`. | | `b` | `Float` | Blue component in sRGB, normalized to `0`–`1`. | | `weight` | `Float` | Share of analyzed pixels this color represents, from `0` to `1`. | Colors are sorted by `weight` in descending order, so the first entry is always the most dominant. The weights of a single call sum to `1`, which lets you treat them as percentages of the block's visible surface. Because the components use the same `0`–`1` range as the rest of the CE.SDK color APIs, you can map a result straight into a SwiftUI `Color`: ```swift let swatches = colors.map { color in Color(red: Double(color.r), green: Double(color.g), blue: Double(color.b)) } ``` ## Configuring the Analysis The optional `options` argument controls how many colors you get back and whether near-white pixels are considered. ```swift let colors = try await engine.block.getDominantColors( block, options: .init(count: 3, ignoreWhite: true) ) ``` | Option | Type | Default | Description | | ------------- | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `count` | `Int` | `5` | Number of colors to extract. The palette may contain fewer entries for images with little variation, and is empty when `count` is `0`. | | `ignoreWhite` | `Bool` | `false` | When `true`, near-white pixels are skipped. Useful for product shots on white backgrounds so the background does not dominate. | ## What the Colors Reflect The analysis runs on the block's rendered output, not its source file. That means: - **Crops, adjustments, and effects are included.** If you crop an image, lower its saturation, or apply a filter, the returned palette reflects the result, not the original asset. - **Transparent pixels are skipped.** Fully or mostly transparent areas are excluded, so a logo on a transparent background returns the logo's colors rather than blending in the empty space. - **The block must be visible.** It has to be attached to a scene and render visible content. Analyzing a detached or fully transparent block returns no colors. ## Troubleshooting ### The call never resolves `getDominantColors(_:options:)` waits for pending assets to finish loading. Make sure the block's resources are reachable — a broken image URI keeps the asset in a pending state. ### The result is empty - Check that `count` is greater than `0`. - Confirm the block is attached to a scene and renders visible content. - A fully transparent block, or one whose only color is filtered out by `ignoreWhite`, returns no colors. ### The colors look different from the source image This is expected. The palette is taken from the rendered block, so any crop, color adjustment, filter, or opacity applied to the block changes the result. ## API Reference | Method | Description | | ------------------------------------------------- | ---------------------------------------------------------------------------- | | `engine.block.getDominantColors(_:options:)` | Returns the block's dominant colors, sorted by weight. Marked `async throws`. | | Type | Properties | Description | | ----------------------- | ----------------------- | -------------------------------------------- | | `DominantColor` | `r`, `g`, `b`, `weight` | A single extracted color and its prominence. | | `DominantColorsOptions` | `count`, `ignoreWhite` | Options controlling the analysis. | ## Next Steps - [Color Basics](https://img.ly/docs/cesdk/mac-catalyst/colors/basics-307115/) — Review the three color spaces CE.SDK supports and when to use each - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) — Apply colors to design elements programmatically --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "For Print" description: "Use print-ready color models and settings for professional-quality, production-ready exports." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/for-print-59bc05/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [For Print](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print-59bc05/) --- --- ## Related Pages - [CMYK Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/cmyk-8a1334/) - Work with CMYK colors in CE.SDK for professional print production workflows with support for color space conversion and tint control. - [Spot Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/spot-c3a150/) - Define, apply, and manage spot colors in CE.SDK for professional print workflows with exact color matching through premixed inks. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "CMYK Colors" description: "Work with CMYK colors in CE.SDK for professional print production workflows with support for color space conversion and tint control." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/cmyk-8a1334/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [For Print](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print-59bc05/) > [CMYK Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/cmyk-8a1334/) --- ```swift file=@cesdk_swift_examples/engine-guides-colors-for-print-cmyk/CMYKColors.swift reference-only import Foundation import IMGLYEngine @MainActor func cmykColors(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // CMYK components (c, m, y, k) and tint all range from 0.0 to 1.0. let cmykCyan = Color.cmyk(c: 1.0, m: 0.0, y: 0.0, k: 0.0, tint: 1.0) let cmykMagenta = Color.cmyk(c: 0.0, m: 1.0, y: 0.0, k: 0.0, tint: 1.0) let cmykYellow = Color.cmyk(c: 0.0, m: 0.0, y: 1.0, k: 0.0, tint: 1.0) let cmykBlack = Color.cmyk(c: 0.0, m: 0.0, y: 0.0, k: 1.0, tint: 1.0) // Create a graphic block, attach a color fill, then assign a CMYK color. // The same setColor call works for any CMYK value. for cmykColor in [cmykCyan, cmykMagenta, cmykYellow, cmykBlack] { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 150) try engine.block.setHeight(block, value: 150) try engine.block.appendChild(to: page, child: block) let fill = try engine.block.createFill(.color) try engine.block.setFill(block, fill: fill) try engine.block.setColor(fill, property: "fill/color/value", color: cmykColor) } // The tint value scales color intensity without changing the CMYK components. // On screen, a tint below 1.0 is rendered as transparency. let tintedBlock = try engine.block.create(.graphic) try engine.block.setShape(tintedBlock, shape: engine.block.createShape(.rect)) try engine.block.appendChild(to: page, child: tintedBlock) let tintedFill = try engine.block.createFill(.color) try engine.block.setFill(tintedBlock, fill: tintedFill) let cmykHalfMagenta = Color.cmyk(c: 0.0, m: 1.0, y: 0.0, k: 0.0, tint: 0.5) try engine.block.setColor(tintedFill, property: "fill/color/value", color: cmykHalfMagenta) // Enable the stroke and set its width before applying a CMYK color. let strokeBlock = try engine.block.create(.graphic) try engine.block.setShape(strokeBlock, shape: engine.block.createShape(.rect)) try engine.block.appendChild(to: page, child: strokeBlock) try engine.block.setStrokeEnabled(strokeBlock, enabled: true) try engine.block.setStrokeWidth(strokeBlock, width: 8) let cmykStrokeColor = Color.cmyk(c: 0.8, m: 0.2, y: 0.0, k: 0.1, tint: 1.0) try engine.block.setColor(strokeBlock, property: "stroke/color", color: cmykStrokeColor) // Enable the drop shadow before applying a CMYK color. let shadowBlock = try engine.block.create(.graphic) try engine.block.setShape(shadowBlock, shape: engine.block.createShape(.rect)) try engine.block.appendChild(to: page, child: shadowBlock) try engine.block.setDropShadowEnabled(shadowBlock, enabled: true) let cmykShadowColor = Color.cmyk(c: 0.0, m: 0.0, y: 0.0, k: 0.6, tint: 0.8) try engine.block.setColor(shadowBlock, property: "dropShadow/color", color: cmykShadowColor) // getColor returns a Color enum. Use pattern matching to inspect the CMYK components. let readBlock = try engine.block.create(.graphic) try engine.block.setShape(readBlock, shape: engine.block.createShape(.rect)) try engine.block.appendChild(to: page, child: readBlock) let readFill = try engine.block.createFill(.color) try engine.block.setFill(readBlock, fill: readFill) let cmykOrange = Color.cmyk(c: 0.0, m: 0.5, y: 1.0, k: 0.0, tint: 1.0) try engine.block.setColor(readFill, property: "fill/color/value", color: cmykOrange) let retrievedColor: Color = try engine.block.getColor(readFill, property: "fill/color/value") if case let .cmyk(c, m, y, k, tint) = retrievedColor { print("CMYK Color - C: \(c), M: \(m), Y: \(y), K: \(k), Tint: \(tint)") } // Convert between sRGB and CMYK using the editor API. Conversions are not // perfectly reversible because the color gamuts differ. // Converting CMYK to sRGB reads the document CMYK profile, which is a resource. Load it once // first, so the conversion does not have to handle COLOR.PROFILE_NOT_LOADED. try await engine.editor.loadColorProfiles() let rgbBlue = Color.rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0) let convertedCmyk = try engine.editor.convertColorToColorSpace(color: rgbBlue, colorSpace: .cmyk) print("RGB to CMYK conversion: \(convertedCmyk)") let cmykGreen = Color.cmyk(c: 0.7, m: 0.0, y: 1.0, k: 0.2, tint: 1.0) let convertedRgb = try engine.editor.convertColorToColorSpace(color: cmykGreen, colorSpace: .sRGB) print("CMYK to RGB conversion: \(convertedRgb)") // CMYK colors can be used in any gradient color stop. let gradientBlock = try engine.block.create(.graphic) try engine.block.setShape(gradientBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(gradientBlock, value: 320) try engine.block.setHeight(gradientBlock, value: 150) try engine.block.appendChild(to: page, child: gradientBlock) let gradientFill = try engine.block.createFill(.linearGradient) try engine.block.setFill(gradientBlock, fill: gradientFill) try engine.block.setGradientColorStops( gradientFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .cmyk(c: 1.0, m: 0.0, y: 0.0, k: 0.0, tint: 1.0), stop: 0.0), GradientColorStop(color: .cmyk(c: 0.0, m: 1.0, y: 0.0, k: 0.0, tint: 1.0), stop: 0.5), GradientColorStop(color: .cmyk(c: 0.0, m: 0.0, y: 1.0, k: 0.0, tint: 1.0), stop: 1.0), ], ) } ``` Work with CMYK colors in CE.SDK for professional print production workflows with support for color space conversion and tint control. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-colors-for-print-cmyk) CMYK (Cyan, Magenta, Yellow, Key/Black) is the standard color model for print production. Unlike sRGB, which is additive and designed for screens, CMYK uses subtractive color mixing to represent how inks combine on paper. CE.SDK represents CMYK as a case of the `Color` enum, so the same `setColor` and `getColor` APIs work across all color spaces. This guide covers how to create CMYK colors, apply them to fills, strokes, and drop shadows, use the `tint` value, read colors back, convert between color spaces, and use CMYK in gradients. ## Understanding CMYK Colors ### When to Use CMYK Use CMYK colors when preparing designs for commercial printing or when print service providers require CMYK values. Screen displays convert CMYK to RGB for preview, but exported PDFs preserve CMYK information for accurate print reproduction. A CMYK color in CE.SDK has five properties: - `c` (Cyan): 0.0 to 1.0 - `m` (Magenta): 0.0 to 1.0 - `y` (Yellow): 0.0 to 1.0 - `k` (Key/Black): 0.0 to 1.0 - `tint`: 0.0 to 1.0 (controls overall color intensity, rendered as transparency on screen) ## Creating CMYK Colors Create a CMYK color with `Color.cmyk(c:m:y:k:tint:)`. Every component ranges from 0.0 to 1.0. ```swift highlight-cmykColors-create // CMYK components (c, m, y, k) and tint all range from 0.0 to 1.0. let cmykCyan = Color.cmyk(c: 1.0, m: 0.0, y: 0.0, k: 0.0, tint: 1.0) let cmykMagenta = Color.cmyk(c: 0.0, m: 1.0, y: 0.0, k: 0.0, tint: 1.0) let cmykYellow = Color.cmyk(c: 0.0, m: 0.0, y: 1.0, k: 0.0, tint: 1.0) let cmykBlack = Color.cmyk(c: 0.0, m: 0.0, y: 0.0, k: 1.0, tint: 1.0) ``` ## Applying CMYK Colors to Fills Apply a CMYK color to a color fill using `engine.block.setColor(_:property:color:)` on the fill with the `"fill/color/value"` property path. ```swift highlight-cmykColors-applyFill // Create a graphic block, attach a color fill, then assign a CMYK color. // The same setColor call works for any CMYK value. for cmykColor in [cmykCyan, cmykMagenta, cmykYellow, cmykBlack] { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 150) try engine.block.setHeight(block, value: 150) try engine.block.appendChild(to: page, child: block) let fill = try engine.block.createFill(.color) try engine.block.setFill(block, fill: fill) try engine.block.setColor(fill, property: "fill/color/value", color: cmykColor) } ``` The same method works for any `Color` case — `.rgba`, `.cmyk`, or `.spot` — so you do not need a separate code path for each color space. ## Using the Tint Property The `tint` value scales the color's intensity without changing its CMYK components. A tint of 1.0 applies the full color; 0.5 scales it down. ```swift highlight-cmykColors-tint // The tint value scales color intensity without changing the CMYK components. // On screen, a tint below 1.0 is rendered as transparency. let tintedBlock = try engine.block.create(.graphic) try engine.block.setShape(tintedBlock, shape: engine.block.createShape(.rect)) try engine.block.appendChild(to: page, child: tintedBlock) let tintedFill = try engine.block.createFill(.color) try engine.block.setFill(tintedBlock, fill: tintedFill) let cmykHalfMagenta = Color.cmyk(c: 0.0, m: 1.0, y: 0.0, k: 0.0, tint: 0.5) try engine.block.setColor(tintedFill, property: "fill/color/value", color: cmykHalfMagenta) ``` > **Note:** When rendered on screen, the tint is applied as transparency. During PDF > export, CMYK colors are currently converted to RGB and the tint is retained in > the alpha channel. ## Applying CMYK to Strokes Enable the stroke and set its width, then assign a CMYK color to the `"stroke/color"` property on the block. ```swift highlight-cmykColors-stroke // Enable the stroke and set its width before applying a CMYK color. let strokeBlock = try engine.block.create(.graphic) try engine.block.setShape(strokeBlock, shape: engine.block.createShape(.rect)) try engine.block.appendChild(to: page, child: strokeBlock) try engine.block.setStrokeEnabled(strokeBlock, enabled: true) try engine.block.setStrokeWidth(strokeBlock, width: 8) let cmykStrokeColor = Color.cmyk(c: 0.8, m: 0.2, y: 0.0, k: 0.1, tint: 1.0) try engine.block.setColor(strokeBlock, property: "stroke/color", color: cmykStrokeColor) ``` ## Applying CMYK to Drop Shadows Enable the drop shadow, then assign a CMYK color to the `"dropShadow/color"` property. Configure offset and blur radius with the usual drop shadow APIs as needed. ```swift highlight-cmykColors-shadow // Enable the drop shadow before applying a CMYK color. let shadowBlock = try engine.block.create(.graphic) try engine.block.setShape(shadowBlock, shape: engine.block.createShape(.rect)) try engine.block.appendChild(to: page, child: shadowBlock) try engine.block.setDropShadowEnabled(shadowBlock, enabled: true) let cmykShadowColor = Color.cmyk(c: 0.0, m: 0.0, y: 0.0, k: 0.6, tint: 0.8) try engine.block.setColor(shadowBlock, property: "dropShadow/color", color: cmykShadowColor) ``` ## Reading CMYK Colors `engine.block.getColor(_:property:)` returns a `Color` enum value. Use pattern matching to extract the CMYK components. ```swift highlight-cmykColors-read // getColor returns a Color enum. Use pattern matching to inspect the CMYK components. let readBlock = try engine.block.create(.graphic) try engine.block.setShape(readBlock, shape: engine.block.createShape(.rect)) try engine.block.appendChild(to: page, child: readBlock) let readFill = try engine.block.createFill(.color) try engine.block.setFill(readBlock, fill: readFill) let cmykOrange = Color.cmyk(c: 0.0, m: 0.5, y: 1.0, k: 0.0, tint: 1.0) try engine.block.setColor(readFill, property: "fill/color/value", color: cmykOrange) let retrievedColor: Color = try engine.block.getColor(readFill, property: "fill/color/value") if case let .cmyk(c, m, y, k, tint) = retrievedColor { print("CMYK Color - C: \(c), M: \(m), Y: \(y), K: \(k), Tint: \(tint)") } ``` Swift's exhaustive enum matching replaces the type-guard helpers used on other platforms. The associated values are available directly inside the matched case. ## Converting Between Color Spaces Use `engine.editor.convertColorToColorSpace(color:colorSpace:)` with `ColorSpace.cmyk` or `ColorSpace.sRGB` to convert between color spaces. ```swift highlight-cmykColors-convert // Convert between sRGB and CMYK using the editor API. Conversions are not // perfectly reversible because the color gamuts differ. // Converting CMYK to sRGB reads the document CMYK profile, which is a resource. Load it once // first, so the conversion does not have to handle COLOR.PROFILE_NOT_LOADED. try await engine.editor.loadColorProfiles() let rgbBlue = Color.rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0) let convertedCmyk = try engine.editor.convertColorToColorSpace(color: rgbBlue, colorSpace: .cmyk) print("RGB to CMYK conversion: \(convertedCmyk)") let cmykGreen = Color.cmyk(c: 0.7, m: 0.0, y: 1.0, k: 0.2, tint: 1.0) let convertedRgb = try engine.editor.convertColorToColorSpace(color: cmykGreen, colorSpace: .sRGB) print("CMYK to RGB conversion: \(convertedRgb)") ``` Conversions may not be perfectly reversible because RGB and CMYK have different color gamuts. Some sRGB colors cannot be represented exactly in CMYK and vice versa. ## Using CMYK in Gradients CMYK colors work in gradient color stops. Create a linear gradient fill and pass `GradientColorStop` values whose `color` is a `Color.cmyk` case. ```swift highlight-cmykColors-gradient // CMYK colors can be used in any gradient color stop. let gradientBlock = try engine.block.create(.graphic) try engine.block.setShape(gradientBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(gradientBlock, value: 320) try engine.block.setHeight(gradientBlock, value: 150) try engine.block.appendChild(to: page, child: gradientBlock) let gradientFill = try engine.block.createFill(.linearGradient) try engine.block.setFill(gradientBlock, fill: gradientFill) try engine.block.setGradientColorStops( gradientFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .cmyk(c: 1.0, m: 0.0, y: 0.0, k: 0.0, tint: 1.0), stop: 0.0), GradientColorStop(color: .cmyk(c: 0.0, m: 1.0, y: 0.0, k: 0.0, tint: 1.0), stop: 0.5), GradientColorStop(color: .cmyk(c: 0.0, m: 0.0, y: 1.0, k: 0.0, tint: 1.0), stop: 1.0), ], ) ``` ## Setting the Document CMYK Profile CE.SDK previews CMYK colors through an ICC profile. The bundled default profile models a US press condition. For another print condition, for example European coated paper, set the CMYK profile of the document to the profile your printer supplies. The engine picks the CMYK profile in this order: 1. A CMYK image with its own embedded ICC profile uses that profile, for that image only. 2. The CMYK profile of the document, when you set one. 3. The profile at the `fallbackCMYKProfileUri` setting. 4. The bundled default profile, when that setting is empty. The document profile and its conversion settings belong to `engine.scene`. They are saved with the scene and bundled into scene archives. Undo and redo do not change these properties. The `fallbackCMYKProfileUri` editor setting belongs to the current editor environment. Raster exports use the document profile for CMYK-to-RGB conversion. A scene whose `scene/colorConversionMode` is `Legacy` keeps its previous conversion until you set the property to `Managed`. ```swift // Load the profile from a URL. The call returns once the profile is in effect. let defaultCMYK = ProfileSlot(role: .default, space: .cmyk) try await engine.scene.setDocumentColorProfile(defaultCMYK, from: URL(string: "https://example.com/profiles/PSOcoated_v3.icc")!) // Or pass the profile bytes directly. let data = try Data(contentsOf: Bundle.main.url(forResource: "PSOcoated_v3", withExtension: "icc")!) try engine.scene.setDocumentColorProfile(defaultCMYK, from: data) // Read what the document names, and go back to the fallback profile. let info = try engine.scene.getDocumentColorProfileInfo(defaultCMYK) // DocumentColorProfileInfo try engine.scene.removeDocumentColorProfile(defaultCMYK) try await engine.editor.loadColorProfiles() // Configure the conversion. try engine.scene.setColorRenderingIntent(.perceptual) engine.scene.setBlackPointCompensationEnabled(false) ``` An assignment is atomic. While a profile loads, the previous profile stays in effect. When the profile cannot be loaded, is not a valid ICC profile, or is not a CMYK profile, the call fails and the previous profile stays in effect. `engine.scene.getDocumentColorProfileInfo(defaultCMYK)` returns the content hash of the profile the document names, its `name` from the ICC description, and the `uri` it is named by, and throws when none is set. A save and load rewrites the URI. A profile that has not finished loading reports no name. `engine.scene.removeDocumentColorProfile(defaultCMYK)` goes back to the fallback profile. A profile set from bytes is saved as a `buffer://` URI when you save the scene to a string, and only the same engine can read that URI. Save the scene to an archive to keep the profile bytes with the scene. To save a URL instead, upload the bytes that `findAllTransientResources` lists and call `relocateResource` before you save. You can also set how colors outside the destination gamut are mapped, with the rendering intent, and whether black point compensation is on. The defaults are relative colorimetric intent with black point compensation. ## Managing Document Profile Slots Use `setDocumentColorProfile(_:from:)` with a `URL` to assign a profile by role and component space, or `setDocumentColorProfile(_:from:)` with `Data` to assign its bytes. A document has six independent slots: Default and Output, each with RGB, CMYK and Gray. For example, `ProfileSlot(role: .output, space: .cmyk)` selects the Output CMYK slot. Read a slot with `getDocumentColorProfileInfo(slot)` and remove it with `removeDocumentColorProfile(slot)`. The getter returns the stored content hash, URI and loaded name. It throws when the slot is empty and does not load the profile or report a fallback. Removal also supersedes a pending assignment to that slot. An assignment or removal in another slot does not cancel it; replacing the scene supersedes all pending assignments. Profiles in every slot persist in scenes and archives. **Only Default CMYK currently affects managed conversion.** The other slots retain profiles without changing rendering or export. A profile must match its slot's component space; a failed assignment keeps the previous profile. Profiles assigned from bytes need an archive to carry their bytes into another engine instance. The CMYK profile functions are deprecated. Use the document color profile methods with the Default CMYK slot for assignment, information and removal. Use `engine.editor.loadColorProfiles()` instead of `loadCMYKProfile()` to wait for the profiles of the document. It opens every profile the document names and resolves the Default CMYK fallback. Only the Default CMYK result decides success or failure. A profile of another slot that fails to load is logged and does not fail the call. ## Troubleshooting ### Colors Look Different on Screen vs. Print Screen previews convert CMYK to RGB using a standard conversion. For accurate color proofing, use calibrated monitors and print proofs from your production workflow. ### Tint Not Having the Expected Effect The `tint` value must be between 0.0 and 1.0. On screen, it is rendered as transparency, so values below 1.0 make the color appear more translucent rather than lighter. ## API Reference | Method | Description | | ----------------------------------------------------------- | ------------------------------------------------------------------------------- | | `engine.block.setColor(_:property:color:)` | Set a color property on a block. Accepts any `Color` case. | | `engine.block.getColor(_:property:)` | Get the current color value from a property. Returns a `Color` enum. | | `engine.editor.convertColorToColorSpace(color:colorSpace:)` | Convert a color between `ColorSpace.sRGB` and `ColorSpace.cmyk`. | | `engine.block.createFill(_:)` | Create a fill. Use `.color` for solid fills or `.linearGradient` for gradients. | | `engine.block.setFill(_:fill:)` | Assign a fill to a block. | | `engine.block.setGradientColorStops(_:property:colors:)` | Set color stops on a gradient fill. | | `engine.editor.loadColorProfiles()` | Wait for every profile the document names and the Default CMYK fallback | | `engine.scene.setDocumentColorProfile(_:from:)` | Make a CMYK ICC profile the document profile, from a `URL` or from `Data` | | `engine.scene.getDocumentColorProfileInfo(defaultCMYK)` | Get the document CMYK profile info; throws if none is set | | `engine.scene.removeDocumentColorProfile(defaultCMYK)` | Remove the document CMYK profile and use the fallback profile | | `engine.scene.setColorRenderingIntent(_:)` | Set the rendering intent of CMYK conversion | | `engine.scene.setBlackPointCompensationEnabled(_:)` | Turn black point compensation on or off | | Type | Description | | ------------------------------------- | ----------------------------------------------------------------- | | `Color.cmyk(c:m:y:k:tint:)` | CMYK color for print. Components and tint range 0.0–1.0. | | `ColorSpace.cmyk` / `ColorSpace.sRGB` | Target color space for `convertColorToColorSpace`. | | `GradientColorStop` | Gradient stop with a `color: Color` and a `stop: Float` position. | ## Next Steps - [Spot Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/spot-c3a150/) — Work with named spot colors for brand consistency and specialized printing - [Color Conversion](https://img.ly/docs/cesdk/mac-catalyst/colors/conversion-bcd82b/) — Convert colors between sRGB, CMYK, and spot color spaces - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) — Apply colors to design elements programmatically --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Spot Colors" description: "Define, apply, and manage spot colors in CE.SDK for professional print workflows with exact color matching through premixed inks." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/spot-c3a150/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [For Print](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print-59bc05/) > [Spot Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/spot-c3a150/) --- ```swift file=@cesdk_swift_examples/engine-guides-spot-colors/SpotColors.swift reference-only import IMGLYEngine @MainActor func spotColors(engine: Engine) async throws { // Demo scaffolding: a scene with a page and three graphic blocks that we // will recolor with spot colors below. A fourth block is added later in the // function to demonstrate the magenta fallback after a spot color is // removed. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let primaryPositions: [(x: Float, y: Float)] = [(40, 180), (285, 180), (530, 180)] var blocks: [DesignBlockID] = [] for position in primaryPositions { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.setWidth(block, value: 230) try engine.block.setHeight(block, value: 240) try engine.block.setPositionX(block, value: position.x) try engine.block.setPositionY(block, value: position.y) try engine.block.appendChild(to: page, child: block) blocks.append(block) } let primaryBlock = blocks[0] let tintBlock = blocks[1] let accentBlock = blocks[2] engine.editor.setSpotColor(name: "Brand-Primary", r: 0.8, g: 0.1, b: 0.2) engine.editor.setSpotColor(name: "Brand-Primary", c: 0.05, m: 0.95, y: 0.85, k: 0.0) engine.editor.setSpotColor(name: "Brand-Accent", r: 0.2, g: 0.4, b: 0.8) engine.editor.setSpotColor(name: "Brand-Accent", c: 0.75, m: 0.5, y: 0.0, k: 0.0) let primaryFill = try engine.block.getFill(primaryBlock) try engine.block.setColor( primaryFill, property: "fill/color/value", color: .spot(name: "Brand-Primary", externalReference: "Brand-Colors"), ) let tintFill = try engine.block.getFill(tintBlock) try engine.block.setColor(tintFill, property: "fill/color/value", color: .spot(name: "Brand-Primary", tint: 0.5)) try await engine.captureGuide(page, label: "after-fills") let accentFill = try engine.block.getFill(accentBlock) try engine.block.setColor(accentFill, property: "fill/color/value", color: .spot(name: "Brand-Accent", tint: 0.3)) try engine.block.setStrokeEnabled(accentBlock, enabled: true) try engine.block.setStrokeWidth(accentBlock, width: 6) try engine.block.setColor(accentBlock, property: "stroke/color", color: .spot(name: "Brand-Accent")) try engine.block.setDropShadowEnabled(accentBlock, enabled: true) try engine.block.setDropShadowOffsetX(accentBlock, offsetX: 6) try engine.block.setDropShadowOffsetY(accentBlock, offsetY: 6) try engine.block.setColor(accentBlock, property: "dropShadow/color", color: .spot(name: "Brand-Primary", tint: 0.6)) try await engine.captureGuide(page, label: "hero") let definedNames = engine.editor.findAllSpotColors() print("Defined spot colors: \(definedNames)") let primaryRGB: RGBA = engine.editor.getSpotColor(name: "Brand-Primary") let primaryCMYK: CMYK = engine.editor.getSpotColor(name: "Brand-Primary") print("Brand-Primary RGB: \(primaryRGB)") print("Brand-Primary CMYK: \(primaryCMYK)") let storedColor: Color = try engine.block.getColor(primaryFill, property: "fill/color/value") if case let .spot(name, tint, _) = storedColor { print("Block is using spot color \(name) at tint \(tint)") } engine.editor.setSpotColor(name: "Brand-Primary", r: 0.85, g: 0.15, b: 0.25) // Add a fourth block colored with a temporary spot color so we can // demonstrate the magenta fallback after the color is removed. let temporaryBlock = try engine.block.create(.graphic) try engine.block.setShape(temporaryBlock, shape: engine.block.createShape(.rect)) try engine.block.setFill(temporaryBlock, fill: engine.block.createFill(.color)) try engine.block.setWidth(temporaryBlock, value: 230) try engine.block.setHeight(temporaryBlock, value: 120) try engine.block.setPositionX(temporaryBlock, value: 285) try engine.block.setPositionY(temporaryBlock, value: 450) try engine.block.appendChild(to: page, child: temporaryBlock) engine.editor.setSpotColor(name: "Temporary-Color", r: 0.3, g: 0.7, b: 0.4) let temporaryFill = try engine.block.getFill(temporaryBlock) try engine.block.setColor(temporaryFill, property: "fill/color/value", color: .spot(name: "Temporary-Color")) try engine.editor.removeSpotColor(name: "Temporary-Color") try await engine.captureGuide(page, label: "after-remove") engine.editor.setSpotColor(name: "DieLine", c: 0.0, m: 1.0, y: 0.0, k: 0.0) try engine.editor.setSpotColorForCutoutType(cutoutType: .solid, name: "DieLine") let assignedName = try engine.editor.getSpotColorForCutoutType(cutoutType: .solid) print("Cutout type .solid uses spot color: \(assignedName)") } ``` Define, apply, and manage spot colors in CE.SDK for professional print workflows with exact color matching through premixed inks. ![Three graphic blocks: a brand spot color on the left, a 50 percent tint in the middle, and a stroked and shadowed block on the right.](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-spot-colors) Spot colors are named colors reproduced using premixed inks in print production, providing exact color matching that CMYK process colors cannot guarantee. CE.SDK maintains a registry of spot color definitions on the editor instance, where each entry has a name and screen approximations (RGB and/or CMYK) for display. The premixed ink is selected at print time based on the spot color name. This guide covers how to define spot colors with RGB and CMYK approximations, apply them to fills, strokes, and shadows, control intensity with tints, query and update definitions, and assign spot colors to cutout types for die-cutting operations. ## Understanding Spot Colors Spot colors differ from CMYK process colors in several important ways: - **Exact color matching** — Premixed inks guarantee consistent color reproduction across print runs. - **Brand consistency** — Essential for logos and corporate brand colors. - **Specialty effects** — Enable metallic, fluorescent, and other specialty inks. - **Color gamut** — Some colors cannot be reproduced with CMYK process inks. A spot color in CE.SDK has four components: - `name` — The identifier used in the print output (for example, `"Brand-Red-485"`). - Approximations — RGB and/or CMYK values used for on-screen display. - `tint` — A value from 0.0 to 1.0 that controls color intensity. - `externalReference` — Optional metadata recording the originating color system (for example, an in-house brand palette identifier or a print vendor's spot-color library code). Carried through to the export but ignored at render time. `Color.spot(name:tint:externalReference:)` is the Swift representation. `tint` defaults to `1` and `externalReference` defaults to `""`. ## Define Spot Colors ### RGB Approximation Register a spot color with `engine.editor.setSpotColor(name:r:g:b:)`. This creates a new spot color if the name doesn't exist, or updates the RGB approximation if it does. Each component ranges from 0.0 to 1.0. ```swift highlight-spotColors-defineRGB engine.editor.setSpotColor(name: "Brand-Primary", r: 0.8, g: 0.1, b: 0.2) ``` RGB approximations control how the spot color is rendered on screen during editing. ### CMYK Approximation Add a CMYK approximation with the `setSpotColor(name:c:m:y:k:)` overload to provide print-accurate previews alongside the RGB display. Calling either overload with an existing name updates that approximation without affecting the other. ```swift highlight-spotColors-defineCMYK engine.editor.setSpotColor(name: "Brand-Primary", c: 0.05, m: 0.95, y: 0.85, k: 0.0) engine.editor.setSpotColor(name: "Brand-Accent", r: 0.2, g: 0.4, b: 0.8) engine.editor.setSpotColor(name: "Brand-Accent", c: 0.75, m: 0.5, y: 0.0, k: 0.0) ``` Provide both approximations whenever possible: RGB drives the on-screen display, while CMYK enables accurate print preview. ## Apply Spot Colors to Design Elements Apply a spot color to any color property with `engine.block.setColor(_:property:color:)` and a `Color.spot(name:tint:externalReference:)` value. The spot color must be defined first — undefined names fall back to magenta on screen. ```swift highlight-spotColors-applyFill let primaryFill = try engine.block.getFill(primaryBlock) try engine.block.setColor( primaryFill, property: "fill/color/value", color: .spot(name: "Brand-Primary", externalReference: "Brand-Colors"), ) ``` The `externalReference` argument is optional metadata describing where the spot color comes from (a named-color system identifier such as a print vendor's spot-color library code, an internal style identifier, and so on). It is preserved by the engine alongside the name but doesn't affect on-screen rendering. `Color` is a single enum across all color spaces, so the same `setColor` method works for `.rgba`, `.cmyk`, and `.spot` values without a separate code path per color space. ### Using Tints `tint` scales the spot color's intensity without changing the underlying name in the print output. A tint of 0.5 produces a 50 percent strength variation; the name in the exported PDF stays the same. ```swift highlight-spotColors-tint let tintFill = try engine.block.getFill(tintBlock) try engine.block.setColor(tintFill, property: "fill/color/value", color: .spot(name: "Brand-Primary", tint: 0.5)) ``` Use tints for lighter variations in a design system rather than defining a separate spot color per shade. ### Strokes and Shadows Spot colors work with any color property — including `"stroke/color"` and `"dropShadow/color"`. Enable the feature, configure offsets and widths as usual, then assign the spot color. ```swift highlight-spotColors-strokeShadow let accentFill = try engine.block.getFill(accentBlock) try engine.block.setColor(accentFill, property: "fill/color/value", color: .spot(name: "Brand-Accent", tint: 0.3)) try engine.block.setStrokeEnabled(accentBlock, enabled: true) try engine.block.setStrokeWidth(accentBlock, width: 6) try engine.block.setColor(accentBlock, property: "stroke/color", color: .spot(name: "Brand-Accent")) try engine.block.setDropShadowEnabled(accentBlock, enabled: true) try engine.block.setDropShadowOffsetX(accentBlock, offsetX: 6) try engine.block.setDropShadowOffsetY(accentBlock, offsetY: 6) try engine.block.setColor(accentBlock, property: "dropShadow/color", color: .spot(name: "Brand-Primary", tint: 0.6)) ``` ## Query Spot Color Definitions ### List and Inspect Approximations Retrieve every defined spot color with `engine.editor.findAllSpotColors()`. Query individual approximations with `engine.editor.getSpotColor(name:)`; the return type is selected by the type annotation — annotate `RGBA` for the RGB approximation or `CMYK` for the CMYK approximation. ```swift highlight-spotColors-query let definedNames = engine.editor.findAllSpotColors() print("Defined spot colors: \(definedNames)") let primaryRGB: RGBA = engine.editor.getSpotColor(name: "Brand-Primary") let primaryCMYK: CMYK = engine.editor.getSpotColor(name: "Brand-Primary") print("Brand-Primary RGB: \(primaryRGB)") print("Brand-Primary CMYK: \(primaryCMYK)") ``` Querying an undefined spot color returns the magenta default for the requested representation. The same magenta value is also returned for a name that *is* defined but only has the other representation set (for example, querying `RGBA` after calling `setSpotColor(name:c:m:y:k:)` only), so check `findAllSpotColors()` to reliably determine whether a name is defined — the returned components alone are not enough. ### Read Colors from Blocks `engine.block.getColor(_:property:)` returns a `Color` enum value. Reusing the `primaryFill` from the Apply Spot Colors to Design Elements step, pattern-match the result to extract the spot color components. ```swift highlight-spotColors-read let storedColor: Color = try engine.block.getColor(primaryFill, property: "fill/color/value") if case let .spot(name, tint, _) = storedColor { print("Block is using spot color \(name) at tint \(tint)") } ``` ## Update and Remove Spot Colors ### Update Approximations Update an existing spot color by calling the set overload again with the same name. This changes how the color appears on screen without changing the name written to the print output, and existing blocks automatically reflect the new approximation. ```swift highlight-spotColors-update engine.editor.setSpotColor(name: "Brand-Primary", r: 0.85, g: 0.15, b: 0.25) ``` ### Remove Spot Colors Remove a spot color with `engine.editor.removeSpotColor(name:)`. The Swift binding is marked `throws` for forward compatibility, but the call currently always succeeds — removing an undefined name is a no-op. ```swift highlight-spotColors-remove try engine.editor.removeSpotColor(name: "Temporary-Color") ``` Removing a spot color doesn't update blocks already using it — they display magenta until the color is redefined or replaced with a different value. ## Spot Colors for Cutouts CE.SDK can assign a spot color to a cutout type for die-cutting and other print finishing operations. Use `engine.editor.setSpotColorForCutoutType(cutoutType:name:)` to associate a defined spot color with `CutoutType.solid` or `CutoutType.dashed`. Query the current assignment with `getSpotColorForCutoutType(cutoutType:)`. ```swift highlight-spotColors-cutout engine.editor.setSpotColor(name: "DieLine", c: 0.0, m: 1.0, y: 0.0, k: 0.0) try engine.editor.setSpotColorForCutoutType(cutoutType: .solid, name: "DieLine") let assignedName = try engine.editor.getSpotColorForCutoutType(cutoutType: .solid) print("Cutout type .solid uses spot color: \(assignedName)") ``` When no assignment is made, `.solid` defaults to `"CutContour"` and `.dashed` defaults to `"PerfCutContour"`. All cutout blocks of the given type render with the assigned spot color immediately after the call. ## Troubleshooting ### Spot Color Displays as Magenta The spot color hasn't been defined. Call `setSpotColor(name:r:g:b:)` or `setSpotColor(name:c:m:y:k:)` with that name before applying it to a block. ### Color Approximation Looks Wrong Call the matching `setSpotColor` overload again with new component values. RGB values drive the on-screen display while CMYK values drive the print preview, so updating one does not change the other. ## API Reference | Method | Description | |--------|-------------| | `engine.editor.setSpotColor(name:r:g:b:)` | Define or update the RGB approximation of a spot color. | | `engine.editor.setSpotColor(name:c:m:y:k:)` | Define or update the CMYK approximation of a spot color. | | `engine.editor.findAllSpotColors()` | Return the names of every defined spot color. | | `engine.editor.getSpotColor(name:) -> RGBA` | Read the RGB approximation. Returns magenta if undefined. | | `engine.editor.getSpotColor(name:) -> CMYK` | Read the CMYK approximation. Returns magenta if undefined. | | `engine.editor.removeSpotColor(name:)` | Remove a spot color from the registry. | | `engine.editor.setSpotColorForCutoutType(cutoutType:name:)` | Assign a spot color to `CutoutType.solid` or `.dashed`. | | `engine.editor.getSpotColorForCutoutType(cutoutType:)` | Read the spot color assigned to a cutout type. | | `engine.block.setColor(_:property:color:)` | Apply a color (including `.spot`) to a block property. | | `engine.block.getColor(_:property:)` | Read a color from a block property. Returns a `Color` enum. | | Type | Description | |------|-------------| | `Color.spot(name:tint:externalReference:)` | Spot color reference. `tint` defaults to `1`; `externalReference` defaults to `""`. | | `CutoutType.solid` / `CutoutType.dashed` | Cutout type values accepted by the cutout APIs. | ## Next Steps - [Export for Printing](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-printing-bca896/) — Export designs with spot colors for professional print production. - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) — Apply colors to fills, strokes, and shadows. - [CMYK Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/cmyk-8a1334/) — Work with CMYK process colors. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "For Screen" description: "Documentation for For Screen" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen-1911f8/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [For Screen](https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen-1911f8/) --- --- ## Related Pages - [sRGB Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen/srgb-e6f59b/) - Work with sRGB colors in CE.SDK for screen-based designs using RGBA values for fills, strokes, shadows, and transparency. - [P3 Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen/p3-706127/) - Documentation for P3 Colors --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "P3 Colors" description: "Documentation for P3 Colors" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen/p3-706127/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [For Screen](https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen-1911f8/) > [P3 Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen/p3-706127/) --- Detect support for the Display P3 wide color gamut and switch the engine into a 16-bit P3 working color space on capable devices. ```swift file=@cesdk_swift_examples/engine-guides-colors-for-screen-p3/P3Colors.swift reference-only import IMGLYEngine @MainActor func p3Colors(engine: Engine) async throws { // Demo scaffolding: a minimal scene to exercise the rendering pipeline. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let p3IsSupported = try engine.editor.supportsP3() do { try engine.editor.checkP3Support() } catch { print("P3 unavailable: \(error.localizedDescription)") // Fall back to sRGB. } if p3IsSupported { try engine.editor.setSettingBool("features/p3WorkingColorSpace", value: true) } do { try engine.editor.checkP3Support() try engine.editor.setSettingBool("features/p3WorkingColorSpace", value: true) } catch { print("Staying on sRGB: \(error.localizedDescription)") } } ``` > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-colors-for-screen-p3) P3 is a wide color gamut that covers roughly 25% more visible colors than sRGB, especially in the reds, oranges, and green-cyan regions. CE.SDK can render and export in a 16-bit Display P3 working space on devices that support it, preserving colors that would otherwise be clipped to sRGB. ## What is P3? The DCI-P3 color space was developed for digital cinema and has been adopted in modern consumer displays, particularly by Apple since 2016. CE.SDK uses the Display P3 variant, which keeps sRGB's gamma curve and replaces only the color primaries. Compared to sRGB: - **Gamut size**: P3 covers about 25% more visible colors - **Primary colors**: P3 red and green are more saturated - **Precision**: The P3 working space uses 16 bits per channel instead of 8, which reduces banding in gradients and color adjustments - **Backwards compatibility**: P3 content displayed on sRGB hardware is automatically converted On sRGB displays, colors are converted for display but the underlying P3 data is preserved in exports. ## Check P3 Support `supportsP3()` returns `true` when the engine can run in the P3 working color space on the current device. Internal errors are coerced to `false`, so the boolean is the only signal you get — use `checkP3Support()` for a diagnostic message. ```swift highlight-p3Colors-checkSupport let p3IsSupported = try engine.editor.supportsP3() ``` P3 is supported on iOS devices, the iOS Simulator, and Mac Catalyst. Only native macOS returns `false` — there the engine continues using its default 8-bit sRGB working space. For a richer diagnostic, use `checkP3Support()` instead. It throws an `Error` whose message explains why P3 is unavailable — typical reasons are *no implementation on the platform*, *no 16-bit float GPU support*, or *no P3-capable display*. ```swift highlight-p3Colors-checkSupportThrowing do { try engine.editor.checkP3Support() } catch { print("P3 unavailable: \(error.localizedDescription)") // Fall back to sRGB. } ``` ## Enable the P3 Working Color Space Switch the engine into a 16-bit Display P3 pipeline by setting the `features/p3WorkingColorSpace` flag. The setting affects everything the editor renders — including the color picker and the live preview — and image exports that contain a wide-gamut image, such as a Display P3 photo, preserve the wider gamut by writing 16-bit Display P3 PNGs with an embedded ICC profile. An export that contains only sRGB or untagged images is an 8-bit sRGB file with the original color values. What you see in the editor matches the exported file; there is no separate preview pipeline. ```swift highlight-p3Colors-enable if p3IsSupported { try engine.editor.setSettingBool("features/p3WorkingColorSpace", value: true) } ``` The setting is silently ignored on platforms where P3 is unavailable, so it is safe to call without a guard, but checking support first avoids enabling a feature that will not take effect. ## Graceful Fallback Combine both calls into a single `do`/`catch` to enable P3 when available and continue in sRGB otherwise. Most applications can stay on this single pattern. ```swift highlight-p3Colors-gracefulFallback do { try engine.editor.checkP3Support() try engine.editor.setSettingBool("features/p3WorkingColorSpace", value: true) } catch { print("Staying on sRGB: \(error.localizedDescription)") } ``` When the device does not support P3, the engine continues using its default 8-bit sRGB working space. Existing colors and exports remain valid; no further code changes are required. ## Effect Colors In the P3 working space, the engine converts the colors of duotone, recolor and green screen effects and of stripe fills into Display P3. LUT filters look up their table in sRGB. Effects therefore show the same colors as in the sRGB working space. Before version 1.85, these effects used their sRGB values as Display P3 values. Duotone, recolor, green screen and stripe fill colors were more saturated, and LUT filters made images paler. To keep that look for existing designs, call `try engine.editor.setSettingBool("features/colorManagedEffectsEnabled", value: false)`. An sRGB export then shows these effects in other colors than the editor, so also turn off sRGB exports. ## sRGB Exports Before version 1.85, every image export in the P3 working space was a 16-bit Display P3 file, also when it contained only sRGB or untagged images. To keep these exports, call `try engine.editor.setSettingBool("features/sRGBExportForSRGBImagesEnabled", value: false)`. ## Platform Support CE.SDK Swift ships on iOS, Mac Catalyst, and macOS. P3 availability differs across these targets: | Target | P3 Working Color Space | | --- | --- | | iOS (device and simulator) | Supported | | Mac Catalyst | Supported | | macOS | Not supported | `supportsP3()` returns the right answer for each target at runtime, so the same code compiles and runs on all three without conditional code. ## P3 vs sRGB: When to Use Each | Use Case | Recommended | | --- | --- | | Native iOS apps targeting Apple devices | P3 | | Mac Catalyst apps where users edit photos | P3 | | Photo or video editing where color accuracy matters | P3 | | Importing photos from modern iPhone cameras | P3 | | Native macOS builds | sRGB | | Smaller export files | sRGB | When the content has a wide-gamut image, the P3 working space exports 16-bit PNGs with an embedded Display P3 ICC profile. The doubled bit depth means files are typically 1.5–2× the size of their 8-bit sRGB equivalents. ## API Reference | Method | Description | | --- | --- | | `engine.editor.supportsP3()` | Returns `true` if the device supports the P3 working color space | | `engine.editor.checkP3Support()` | Throws an error describing why P3 is unavailable; returns normally when supported | | `engine.editor.setSettingBool("features/p3WorkingColorSpace", value: true)` | Enables the 16-bit Display P3 working color space; silently ignored where P3 is unsupported | ## Next Steps - [sRGB Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen/srgb-e6f59b/) — Apply sRGB colors for screen output - [Color Conversion](https://img.ly/docs/cesdk/mac-catalyst/colors/conversion-bcd82b/) — Convert colors between sRGB and CMYK --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "sRGB Colors" description: "Work with sRGB colors in CE.SDK for screen-based designs using RGBA values for fills, strokes, shadows, and transparency." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen/srgb-e6f59b/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [For Screen](https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen-1911f8/) > [sRGB Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-screen/srgb-e6f59b/) --- Apply sRGB colors to design elements for screen-based output using RGBA values with red, green, blue, and alpha components. ![A blue rectangle with a red stroke and a semi-transparent black drop shadow](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-colors-for-screen-srgb) sRGB is the standard color space for screen displays. CE.SDK represents sRGB colors with the `Color.rgba` case, where each component uses floating-point values between `0.0` and `1.0` — not the traditional `0`–`255` integer range used in many design tools. ```swift file=@cesdk_swift_examples/engine-guides-colors-for-screen-srgb/SrgbColors.swift reference-only import IMGLYEngine @MainActor func srgbColors(engine: Engine) async throws { // Demo scaffolding: a scene with a page and a single graphic block to recolor. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.setWidth(block, value: 400) try engine.block.setHeight(block, value: 300) try engine.block.setPositionX(block, value: 200) try engine.block.setPositionY(block, value: 150) try engine.block.appendChild(to: page, child: block) let rgbaBlue = Color.rgba(r: 0.2, g: 0.4, b: 0.9) let rgbaRed = Color.rgba(r: 0.85, g: 0.1, b: 0.1, a: 1.0) let semiTransparentBlack = Color.rgba(r: 0.0, g: 0.0, b: 0.0, a: 0.5) let fill = try engine.block.getFill(block) try engine.block.setColor(fill, property: "fill/color/value", color: rgbaBlue) try await engine.captureGuide(page, label: "after-fill") try engine.block.setStrokeEnabled(block, enabled: true) try engine.block.setStrokeWidth(block, width: 8) try engine.block.setColor(block, property: "stroke/color", color: rgbaRed) try await engine.captureGuide(page, label: "after-stroke") try engine.block.setDropShadowEnabled(block, enabled: true) try engine.block.setDropShadowOffsetX(block, offsetX: 15) try engine.block.setDropShadowOffsetY(block, offsetY: 15) try engine.block.setColor(block, property: "dropShadow/color", color: semiTransparentBlack) try await engine.captureGuide(page, label: "hero") let currentColor: Color = try engine.block.getColor(fill, property: "fill/color/value") print("Current fill color: \(currentColor)") if case let .rgba(r, g, b, a) = currentColor { print("sRGB color - r: \(r), g: \(g), b: \(b), a: \(a)") } // Converting CMYK reads the document CMYK profile, which is a resource. Load it once first. try await engine.editor.loadColorProfiles() let cmykOrange = Color.cmyk(c: 0.0, m: 0.5, y: 1.0, k: 0.0, tint: 1.0) let convertedToSrgb = try engine.editor.convertColorToColorSpace(color: cmykOrange, colorSpace: .sRGB) print("CMYK converted to sRGB: \(convertedToSrgb)") } ``` This guide covers creating RGBA color values, working with transparency, applying them to fills, strokes, and shadows, retrieving colors from elements, identifying RGBA colors, and converting other color spaces to sRGB. ## Creating sRGB Colors Programmatically Create an sRGB color using `Color.rgba`. All four components (`r`, `g`, `b`, `a`) take floating-point values from `0.0` to `1.0`. ```swift highlight-srgbColors-createRgba let rgbaBlue = Color.rgba(r: 0.2, g: 0.4, b: 0.9) let rgbaRed = Color.rgba(r: 0.85, g: 0.1, b: 0.1, a: 1.0) ``` The `a` parameter defaults to `1.0`, so you can omit it for fully opaque colors. ## Working with Transparency The alpha component controls transparency: `1.0` is fully opaque and `0.0` is fully transparent. Use values in between for overlays and layered effects. ```swift highlight-srgbColors-createTransparent let semiTransparentBlack = Color.rgba(r: 0.0, g: 0.0, b: 0.0, a: 0.5) ``` ## Applying sRGB Colors to Fills To color a block's fill, first get the fill block with `engine.block.getFill`, then call `engine.block.setColor` with the `"fill/color/value"` property path. ```swift highlight-srgbColors-applyFill let fill = try engine.block.getFill(block) try engine.block.setColor(fill, property: "fill/color/value", color: rgbaBlue) ``` The fill is a separate block from the design block. Color properties live on the fill, not on the parent — applying `"fill/color/value"` to the parent throws. ## Applying sRGB Colors to Strokes Stroke colors are applied directly to the design block using the `"stroke/color"` property path. Enable the stroke first with `setStrokeEnabled`; without it, the color is stored but nothing visible renders. ```swift highlight-srgbColors-applyStroke try engine.block.setStrokeEnabled(block, enabled: true) try engine.block.setStrokeWidth(block, width: 8) try engine.block.setColor(block, property: "stroke/color", color: rgbaRed) ``` Use `setStrokeWidth` to control the line thickness. ## Applying sRGB Colors to Shadows Drop shadow colors use the `"dropShadow/color"` property on the design block. Enable shadows first with `setDropShadowEnabled`. ```swift highlight-srgbColors-applyShadow try engine.block.setDropShadowEnabled(block, enabled: true) try engine.block.setDropShadowOffsetX(block, offsetX: 15) try engine.block.setDropShadowOffsetY(block, offsetY: 15) try engine.block.setColor(block, property: "dropShadow/color", color: semiTransparentBlack) ``` Control the shadow position with `setDropShadowOffsetX` and `setDropShadowOffsetY`. A semi-transparent black creates a natural shadow effect. ## Retrieving Colors from Elements Read a block's current color with `engine.block.getColor`. The return type is `Color` — the same enum used when setting — so a single value carries its color space along with its components. ```swift highlight-srgbColors-getColor let currentColor: Color = try engine.block.getColor(fill, property: "fill/color/value") print("Current fill color: \(currentColor)") ``` Swift's overload resolution can't pick between the deprecated `RGBA`-returning overload and the canonical `Color` one without help, so annotate the binding (`let currentColor: Color = ...`). ## Identifying sRGB Colors Swift's `Color` is an enum with one case per color space. Use pattern matching against `.rgba` to check whether a color is sRGB and read out its components. ```swift highlight-srgbColors-identifyRgba if case let .rgba(r, g, b, a) = currentColor { print("sRGB color - r: \(r), g: \(g), b: \(b), a: \(a)") } ``` ## Converting Colors to sRGB Use `engine.editor.convertColorToColorSpace` to convert CMYK or spot colors to sRGB for screen display. ```swift highlight-srgbColors-convertToSrgb // Converting CMYK reads the document CMYK profile, which is a resource. Load it once first. try await engine.editor.loadColorProfiles() let cmykOrange = Color.cmyk(c: 0.0, m: 0.5, y: 1.0, k: 0.0, tint: 1.0) let convertedToSrgb = try engine.editor.convertColorToColorSpace(color: cmykOrange, colorSpace: .sRGB) print("CMYK converted to sRGB: \(convertedToSrgb)") ``` Color conversions are approximations because CMYK has a smaller gamut than sRGB, so vibrant colors may appear muted after conversion. ## Troubleshooting ### `getColor` Does Not Compile Annotate the binding as `Color` so Swift picks the canonical overload instead of the deprecated `RGBA`-returning one: `let currentColor: Color = try engine.block.getColor(fill, property: "fill/color/value")`. ## API Reference | Method | Description | |--------|-------------| | `Color.rgba(r:g:b:a:)` | Create an sRGB color from `0.0`–`1.0` components | | `engine.block.setColor(_:property:color:)` | Apply a color to a block property | | `engine.block.getColor(_:property:)` | Read the current color from a block property (annotate as `Color`) | | `engine.block.getFill(_:)` | Get the fill block of a design block | | `engine.block.setStrokeEnabled(_:enabled:)` | Enable or disable stroke on a block | | `engine.block.setStrokeWidth(_:width:)` | Set stroke thickness | | `engine.block.setDropShadowEnabled(_:enabled:)` | Enable or disable drop shadow on a block | | `engine.editor.convertColorToColorSpace(color:colorSpace:)` | Convert a color to a different color space | ## Next Steps - [CMYK Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/cmyk-8a1334/) — Work with CMYK for print workflows - [Spot Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/spot-c3a150/) — Use named spot colors for brand consistency - [Color Conversion](https://img.ly/docs/cesdk/mac-catalyst/colors/conversion-bcd82b/) — Convert colors between sRGB, CMYK, and spot color spaces - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) — Comprehensive color application guide --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Overview" description: "Manage color usage in your designs, from applying brand palettes to handling print and screen formats." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/overview-16a177/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/colors/overview-16a177/) --- Colors are a fundamental part of design in the CreativeEditor SDK (CE.SDK). Whether you're designing for digital screens or printed materials, consistent color management ensures your creations look the way you intend. CE.SDK offers flexible tools for working with colors through both the user interface and programmatically, making it easy to manage color workflows at any scale. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## For Print and Screen Designing for screen and designing for print involve different color requirements, and CreativeEditor SDK (CE.SDK) is built to support both. - **Screen workflows** use RGB-based color spaces like sRGB and Display P3. These spaces are optimized for digital displays, ensuring vibrant, consistent colors across different devices. - **Print workflows** typically rely on CMYK and Spot Colors. These spaces reflect how physical inks combine on paper and require more precise color control to match print output. CE.SDK makes it easy to design for both by: - Supporting inputs in multiple color spaces, including sRGB, CMYK, and Spot Colors. - Automatically managing color conversions between spaces. - Providing tooling to ensure that exported files are optimized for either screen (e.g., PNG, WebP) or print (e.g., PDF with Spot Color support). > **Note:** **Note:** CE.SDK allows you to specify colors using CMYK values, but these are > always converted to RGB internally. You cannot produce a true CMYK PDF with > CE.SDK, and we do not support print-specific PDF variants like PDF/X. However, > Spot Colors are fully supported and properly embedded in exported PDFs. ## Color Management Color management is the process of controlling how colors are represented across different devices and outputs—such as screens, printers, and files—to ensure that the colors you see during design match the final result. In CreativeEditor SDK (CE.SDK), color management includes: - Supporting multiple input color spaces (e.g., sRGB, Display P3 for screens, CMYK, Spot Colors for print). - Handling accurate conversions between color spaces (e.g., RGB ↔ CMYK, approximating Spot Colors). - Allowing custom color palettes and color libraries to maintain consistent color choices across a project or brand. - Providing tools to adjust color properties like brightness, contrast, and temperature. - Ensuring consistent color appearance across different export formats and output mediums. Color management ensures that your designs maintain color accuracy and visual integrity whether they are displayed on a screen or printed professionally. ## Color Libraries and Custom Palettes Color libraries in CE.SDK are collections of predefined colors that appear in the color picker, helping users maintain brand or project consistency. Libraries are implemented as asset sources, where each color is represented as an individual asset. Each color library is identified by a unique source ID, allowing you to create, configure, and manage custom palettes tailored to your design needs. This setup makes it easy to offer curated color options to end users, improving both usability and design consistency. ## Supported Color Spaces CreativeEditor SDK supports a wide range of color spaces to meet different design requirements: - **sRGB:** The standard color space for most digital screens. - **Display P3:** A wider gamut color space often used on high-end displays (not available on the Web and server platforms). - **CMYK:** A subtractive color model used primarily for print workflows. - **Spot Colors:** Special premixed inks for print-specific designs requiring precise color matching. Each color space serves different use cases, ensuring that your designs remain accurate whether viewed on a screen or produced in print. ## Applying and Modifying Colors CE.SDK allows you to apply and modify colors both through the UI and programmatically via the API. You can set colors for fills, strokes, outlines, and other properties dynamically, enabling a wide range of creative control. Whether you are adjusting a single design element or programmatically updating entire templates, the SDK provides the flexibility you need. ## Color Adjustments and Properties CreativeEditor SDK provides a range of color adjustments to fine-tune your designs. Commonly used adjustments include: - **Brightness:** Adjust the overall lightness or darkness. - **Contrast:** Adjust the difference between light and dark areas. - **Saturation:** Increase or decrease the intensity of colors. - **Exposure:** Modify the overall exposure level. - **Temperature:** Adjust the color balance between warm and cool tones. - **Sharpness:** Enhance the crispness and clarity of edges. In addition to these, CE.SDK supports many more detailed adjustments. You can also modify specific color-related properties like fill color, stroke color, and opacity. All adjustments and property changes can be applied both through the UI and programmatically. ## Color Conversion CreativeEditor SDK automatically manages color conversions to help ensure consistent visual output across different mediums. The following conversion behaviors are supported: - **RGB and CMYK colors** can be converted between each other using a mathematical approximation. - **Spot colors** can be represented in RGB and CMYK by using a corresponding color approximation. - **RGB and CMYK colors cannot be converted to spot colors.** These automatic conversions help ensure that designs maintain visual consistency whether they are viewed on a digital screen or prepared for professional printing. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Replace Individual Colors" description: "Documentation for Replace Individual Colors" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/colors/replace-48cd71/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) > [Replace Individual Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/replace-48cd71/) --- Selectively replace specific colors in images using CE.SDK's Recolor and Green Screen effects. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-colors-replace) CE.SDK provides two effects for selective color modification: the **Recolor** effect swaps pixels matching a source color for a target color, while the **Green Screen** effect removes pixels matching a specified color to create transparency. Both effects use configurable tolerance parameters to control which pixels are affected, enabling use cases from product color variations to background removal. ```swift file=@cesdk_swift_examples/engine-guides-colors-replace/ColorsReplace.swift reference-only import Foundation import IMGLYEngine @MainActor func colorsReplace(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 450) try engine.block.appendChild(to: scene, child: page) let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") // Create a Recolor effect that swaps red pixels for blue, then attach it to // an image block using `appendEffect`. let recolorBlock = try engine.block.create(.graphic) try engine.block.setShape(recolorBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(recolorBlock, value: 50) try engine.block.setPositionY(recolorBlock, value: 50) try engine.block.setWidth(recolorBlock, value: 200) try engine.block.setHeight(recolorBlock, value: 150) try engine.block.appendChild(to: page, child: recolorBlock) let recolorFill = try engine.block.createFill(.image) try engine.block.setURL(recolorFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(recolorBlock, fill: recolorFill) let recolorEffect = try engine.block.createEffect(.recolor) try engine.block.setColor( recolorEffect, property: "effect/recolor/fromColor", color: .rgba(r: 1, g: 0, b: 0, a: 1), ) try engine.block.setColor( recolorEffect, property: "effect/recolor/toColor", color: .rgba(r: 0, g: 0.5, b: 1, a: 1), ) try engine.block.appendEffect(recolorBlock, effectID: recolorEffect) try await engine.captureGuide(page, label: "after-recolor") let tolerancesBlock = try engine.block.create(.graphic) try engine.block.setShape(tolerancesBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(tolerancesBlock, value: 300) try engine.block.setPositionY(tolerancesBlock, value: 50) try engine.block.setWidth(tolerancesBlock, value: 200) try engine.block.setHeight(tolerancesBlock, value: 150) try engine.block.appendChild(to: page, child: tolerancesBlock) let tolerancesFill = try engine.block.createFill(.image) try engine.block.setURL(tolerancesFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(tolerancesBlock, fill: tolerancesFill) let tolerancesEffect = try engine.block.createEffect(.recolor) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/colorMatch", value: 0.3) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/brightnessMatch", value: 0.2) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/smoothness", value: 0.1) try engine.block.setColor( tolerancesEffect, property: "effect/recolor/fromColor", color: .rgba(r: 0.8, g: 0.6, b: 0.4, a: 1), ) try engine.block.setColor( tolerancesEffect, property: "effect/recolor/toColor", color: .rgba(r: 0.3, g: 0.7, b: 0.3, a: 1), ) try engine.block.appendEffect(tolerancesBlock, effectID: tolerancesEffect) // Create a Green Screen effect. `fromColor` picks the color to remove; any // pixel close enough to that color becomes transparent. let greenScreenBlock = try engine.block.create(.graphic) try engine.block.setShape(greenScreenBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(greenScreenBlock, value: 550) try engine.block.setPositionY(greenScreenBlock, value: 50) try engine.block.setWidth(greenScreenBlock, value: 200) try engine.block.setHeight(greenScreenBlock, value: 150) try engine.block.appendChild(to: page, child: greenScreenBlock) let greenScreenFill = try engine.block.createFill(.image) try engine.block.setURL(greenScreenFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(greenScreenBlock, fill: greenScreenFill) let greenScreenEffect = try engine.block.createEffect(.greenScreen) try engine.block.setColor( greenScreenEffect, property: "effect/green_screen/fromColor", color: .rgba(r: 0, g: 1, b: 0, a: 1), ) try engine.block.appendEffect(greenScreenBlock, effectID: greenScreenEffect) try await engine.captureGuide(page, label: "after-green-screen") let spillBlock = try engine.block.create(.graphic) try engine.block.setShape(spillBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(spillBlock, value: 50) try engine.block.setPositionY(spillBlock, value: 250) try engine.block.setWidth(spillBlock, value: 200) try engine.block.setHeight(spillBlock, value: 150) try engine.block.appendChild(to: page, child: spillBlock) let spillFill = try engine.block.createFill(.image) try engine.block.setURL(spillFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(spillBlock, fill: spillFill) let spillEffect = try engine.block.createEffect(.greenScreen) try engine.block.setFloat(spillEffect, property: "effect/green_screen/colorMatch", value: 0.4) try engine.block.setFloat(spillEffect, property: "effect/green_screen/smoothness", value: 0.2) try engine.block.setFloat(spillEffect, property: "effect/green_screen/spill", value: 0.5) try engine.block.setColor( spillEffect, property: "effect/green_screen/fromColor", color: .rgba(r: 0.2, g: 0.8, b: 0.3, a: 1), ) try engine.block.appendEffect(spillBlock, effectID: spillEffect) // Stack multiple Recolor effects on a single block, then toggle individual // entries with `setEffectEnabled` without removing them from the stack. let stackedBlock = try engine.block.create(.graphic) try engine.block.setShape(stackedBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(stackedBlock, value: 300) try engine.block.setPositionY(stackedBlock, value: 250) try engine.block.setWidth(stackedBlock, value: 200) try engine.block.setHeight(stackedBlock, value: 150) try engine.block.appendChild(to: page, child: stackedBlock) let stackedFill = try engine.block.createFill(.image) try engine.block.setURL(stackedFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(stackedBlock, fill: stackedFill) let redToBlue = try engine.block.createEffect(.recolor) try engine.block.setColor(redToBlue, property: "effect/recolor/fromColor", color: .rgba(r: 1, g: 0, b: 0, a: 1)) try engine.block.setColor(redToBlue, property: "effect/recolor/toColor", color: .rgba(r: 0, g: 0, b: 1, a: 1)) try engine.block.appendEffect(stackedBlock, effectID: redToBlue) let greenToOrange = try engine.block.createEffect(.recolor) try engine.block.setColor(greenToOrange, property: "effect/recolor/fromColor", color: .rgba(r: 0, g: 1, b: 0, a: 1)) try engine.block.setColor(greenToOrange, property: "effect/recolor/toColor", color: .rgba(r: 1, g: 0.5, b: 0, a: 1)) try engine.block.appendEffect(stackedBlock, effectID: greenToOrange) let stackedEffects = try engine.block.getEffects(stackedBlock) print("Number of effects: \(stackedEffects.count)") // 2 try engine.block.setEffectEnabled(effectID: stackedEffects[0], enabled: false) let isEnabled = try engine.block.isEffectEnabled(effectID: stackedEffects[0]) print("First effect enabled: \(isEnabled)") // false // Apply a consistent Recolor effect to every graphic block in the scene. // Skip blocks that already carry an effect so existing work isn't overwritten. let batchBlock = try engine.block.create(.graphic) try engine.block.setShape(batchBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(batchBlock, value: 550) try engine.block.setPositionY(batchBlock, value: 250) try engine.block.setWidth(batchBlock, value: 200) try engine.block.setHeight(batchBlock, value: 150) try engine.block.appendChild(to: page, child: batchBlock) let batchFill = try engine.block.createFill(.image) try engine.block.setURL(batchFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(batchBlock, fill: batchFill) let allGraphicBlocks = try engine.block.find(byType: .graphic) for blockID in allGraphicBlocks { if try engine.block.getEffects(blockID).isEmpty == false { continue } let batchRecolor = try engine.block.createEffect(.recolor) try engine.block.setColor( batchRecolor, property: "effect/recolor/fromColor", color: .rgba(r: 0.8, g: 0.7, b: 0.6, a: 1), ) try engine.block.setColor( batchRecolor, property: "effect/recolor/toColor", color: .rgba(r: 0.6, g: 0.7, b: 0.9, a: 1), ) try engine.block.setFloat(batchRecolor, property: "effect/recolor/colorMatch", value: 0.25) try engine.block.appendEffect(blockID, effectID: batchRecolor) } try await engine.captureGuide(page, label: "hero") } ``` This guide covers how to apply and manage color replacement effects programmatically using the block API. ## Setup We start with a scene and a page. Each example in this guide adds its own image block so the effects can be compared side-by-side. ```swift highlight-colorsReplace-setup let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 450) try engine.block.appendChild(to: scene, child: page) let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") ``` ## Programmatic Color Replacement Effects are themselves design blocks: create one with `createEffect`, configure its properties, then attach it to a target block with `appendEffect`. ### Creating a Recolor Effect The Recolor effect replaces pixels matching a source color with a target color. Use `createEffect(.recolor)`, then set the `fromColor` and `toColor` properties with `setColor`. ```swift highlight-colorsReplace-createRecolor // Create a Recolor effect that swaps red pixels for blue, then attach it to // an image block using `appendEffect`. let recolorBlock = try engine.block.create(.graphic) try engine.block.setShape(recolorBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(recolorBlock, value: 50) try engine.block.setPositionY(recolorBlock, value: 50) try engine.block.setWidth(recolorBlock, value: 200) try engine.block.setHeight(recolorBlock, value: 150) try engine.block.appendChild(to: page, child: recolorBlock) let recolorFill = try engine.block.createFill(.image) try engine.block.setURL(recolorFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(recolorBlock, fill: recolorFill) let recolorEffect = try engine.block.createEffect(.recolor) try engine.block.setColor( recolorEffect, property: "effect/recolor/fromColor", color: .rgba(r: 1, g: 0, b: 0, a: 1), ) try engine.block.setColor( recolorEffect, property: "effect/recolor/toColor", color: .rgba(r: 0, g: 0.5, b: 1, a: 1), ) try engine.block.appendEffect(recolorBlock, effectID: recolorEffect) ``` `fromColor` specifies which color to match in the image, and `toColor` defines the replacement color. Colors use RGBA components in the `0...1` range. ### Configuring Color Matching Precision Adjust the tolerance parameters with `setFloat` to fine-tune which pixels are affected: ```swift highlight-colorsReplace-configureRecolor let tolerancesEffect = try engine.block.createEffect(.recolor) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/colorMatch", value: 0.3) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/brightnessMatch", value: 0.2) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/smoothness", value: 0.1) ``` The Recolor effect exposes three precision parameters: | Property | Range | Description | |----------|-------|-------------| | `effect/recolor/colorMatch` | 0–1 | Hue tolerance. Higher values include more color variations around the source color. | | `effect/recolor/brightnessMatch` | 0–1 | Luminance tolerance. Higher values include pixels with different brightness levels. | | `effect/recolor/smoothness` | 0–1 | Edge blending. Higher values create softer transitions at the boundaries of affected areas. | ### Creating a Green Screen Effect The Green Screen effect removes pixels matching a specified color, making them transparent. This is commonly used for background removal. ```swift highlight-colorsReplace-createGreenScreen // Create a Green Screen effect. `fromColor` picks the color to remove; any // pixel close enough to that color becomes transparent. let greenScreenBlock = try engine.block.create(.graphic) try engine.block.setShape(greenScreenBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(greenScreenBlock, value: 550) try engine.block.setPositionY(greenScreenBlock, value: 50) try engine.block.setWidth(greenScreenBlock, value: 200) try engine.block.setHeight(greenScreenBlock, value: 150) try engine.block.appendChild(to: page, child: greenScreenBlock) let greenScreenFill = try engine.block.createFill(.image) try engine.block.setURL(greenScreenFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(greenScreenBlock, fill: greenScreenFill) let greenScreenEffect = try engine.block.createEffect(.greenScreen) try engine.block.setColor( greenScreenEffect, property: "effect/green_screen/fromColor", color: .rgba(r: 0, g: 1, b: 0, a: 1), ) try engine.block.appendEffect(greenScreenBlock, effectID: greenScreenEffect) ``` Set the `fromColor` property to specify which color to remove. Matching pixels become transparent. ### Configuring Green Screen Parameters Control removal precision with the Green Screen tolerance parameters: ```swift highlight-colorsReplace-configureGreenScreen let spillEffect = try engine.block.createEffect(.greenScreen) try engine.block.setFloat(spillEffect, property: "effect/green_screen/colorMatch", value: 0.4) try engine.block.setFloat(spillEffect, property: "effect/green_screen/smoothness", value: 0.2) try engine.block.setFloat(spillEffect, property: "effect/green_screen/spill", value: 0.5) ``` | Property | Description | |----------|-------------| | `effect/green_screen/colorMatch` | Tolerance for matching the background color. | | `effect/green_screen/smoothness` | Edge softness for cleaner cutouts around subjects. | | `effect/green_screen/spill` | Reduces color bleed from the removed background onto the subject, useful when the background color reflects onto edges. | ## Managing Multiple Effects A single block can have multiple effects applied. Use the effect management APIs to list, toggle, and remove effects. ```swift highlight-colorsReplace-manageEffects // Stack multiple Recolor effects on a single block, then toggle individual // entries with `setEffectEnabled` without removing them from the stack. let stackedBlock = try engine.block.create(.graphic) try engine.block.setShape(stackedBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(stackedBlock, value: 300) try engine.block.setPositionY(stackedBlock, value: 250) try engine.block.setWidth(stackedBlock, value: 200) try engine.block.setHeight(stackedBlock, value: 150) try engine.block.appendChild(to: page, child: stackedBlock) let stackedFill = try engine.block.createFill(.image) try engine.block.setURL(stackedFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(stackedBlock, fill: stackedFill) let redToBlue = try engine.block.createEffect(.recolor) try engine.block.setColor(redToBlue, property: "effect/recolor/fromColor", color: .rgba(r: 1, g: 0, b: 0, a: 1)) try engine.block.setColor(redToBlue, property: "effect/recolor/toColor", color: .rgba(r: 0, g: 0, b: 1, a: 1)) try engine.block.appendEffect(stackedBlock, effectID: redToBlue) let greenToOrange = try engine.block.createEffect(.recolor) try engine.block.setColor(greenToOrange, property: "effect/recolor/fromColor", color: .rgba(r: 0, g: 1, b: 0, a: 1)) try engine.block.setColor(greenToOrange, property: "effect/recolor/toColor", color: .rgba(r: 1, g: 0.5, b: 0, a: 1)) try engine.block.appendEffect(stackedBlock, effectID: greenToOrange) let stackedEffects = try engine.block.getEffects(stackedBlock) print("Number of effects: \(stackedEffects.count)") // 2 try engine.block.setEffectEnabled(effectID: stackedEffects[0], enabled: false) let isEnabled = try engine.block.isEffectEnabled(effectID: stackedEffects[0]) print("First effect enabled: \(isEnabled)") // false ``` Key effect management methods: - `getEffects(_:)` returns every effect ID attached to a block. - `setEffectEnabled(effectID:enabled:)` toggles an effect on or off without removing it. - `isEffectEnabled(effectID:)` checks whether an effect is currently active. - `removeEffect(_:index:)` removes an effect by its position in the effect stack. Stacking multiple Recolor effects enables complex color transformations, such as replacing several colors in a single image. ## Batch Processing Apply the same color replacement configuration to every graphic block in a scene. Use `find(byType:)` to locate the target blocks and skip any that already carry effects so existing work isn't overwritten. ```swift highlight-colorsReplace-batchProcessing // Apply a consistent Recolor effect to every graphic block in the scene. // Skip blocks that already carry an effect so existing work isn't overwritten. let batchBlock = try engine.block.create(.graphic) try engine.block.setShape(batchBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(batchBlock, value: 550) try engine.block.setPositionY(batchBlock, value: 250) try engine.block.setWidth(batchBlock, value: 200) try engine.block.setHeight(batchBlock, value: 150) try engine.block.appendChild(to: page, child: batchBlock) let batchFill = try engine.block.createFill(.image) try engine.block.setURL(batchFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(batchBlock, fill: batchFill) let allGraphicBlocks = try engine.block.find(byType: .graphic) for blockID in allGraphicBlocks { if try engine.block.getEffects(blockID).isEmpty == false { continue } let batchRecolor = try engine.block.createEffect(.recolor) try engine.block.setColor( batchRecolor, property: "effect/recolor/fromColor", color: .rgba(r: 0.8, g: 0.7, b: 0.6, a: 1), ) try engine.block.setColor( batchRecolor, property: "effect/recolor/toColor", color: .rgba(r: 0.6, g: 0.7, b: 0.9, a: 1), ) try engine.block.setFloat(batchRecolor, property: "effect/recolor/colorMatch", value: 0.25) try engine.block.appendEffect(blockID, effectID: batchRecolor) } ``` ## Troubleshooting **Colors not matching as expected**: Increase the `colorMatch` tolerance for broader selection, or decrease it for more precise matching. Check that your source color closely matches the actual color in the image. **Harsh edges around replaced areas**: Increase the `smoothness` value to create softer transitions at the boundaries of affected pixels. **Color spill on Green Screen subjects**: Increase the `spill` value to reduce the green tint that often appears on edges when removing green backgrounds. **Effect not visible**: Verify that the effect is enabled with `isEffectEnabled(effectID:)` and has been appended to the block with `appendEffect(_:effectID:)`. ## API Reference | Method | Description | |--------|-------------| | `block.createEffect(.recolor)` | Create a new Recolor effect block. | | `block.createEffect(.greenScreen)` | Create a new Green Screen effect block. | | `block.appendEffect(_:effectID:)` | Add an effect to a block's effect stack. | | `block.getEffects(_:)` | Get all effects applied to a block. | | `block.removeEffect(_:index:)` | Remove an effect at a specific index. | | `block.setEffectEnabled(effectID:enabled:)` | Enable or disable an effect. | | `block.isEffectEnabled(effectID:)` | Check if an effect is enabled. | | `block.setColor(_:property:color:)` | Set a color property on an effect. | | `block.setFloat(_:property:value:)` | Set a float property on an effect. | | `block.find(byType:)` | Find blocks by type for batch processing. | ## Next Steps - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) — Apply solid colors, gradients, and fills to blocks - [Apply a Filter or Effect](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) — Explore other image effects like filters and adjustments - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Export processed images in various formats --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "System Compatibility" description: "Learn how device performance and hardware limits affect CE.SDK editing, rendering, and export capabilities." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/compatibility-139ef9/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Compatibility & Security](https://img.ly/docs/cesdk/mac-catalyst/compatibility-fef719/) > [System Compatibility](https://img.ly/docs/cesdk/mac-catalyst/compatibility-139ef9/) --- CE.SDK runs entirely on clients and makes use of hardware acceleration provided within that environment. Therefore, the user’s hardware always acts as an upper bound of what’s achievable. The editor's performance scales with scene complexity. We generally found scenes with up to 200 blocks well usable, but complex blocks like auto-sizing text or high-resolution image fills may affect performance negatively. This is always constrained by the processing power available on the user's device, so for low-end devices, the experience may suffer earlier. Therefore, it’s generally desirable to keep scenes only as complex as needed. ## Hardware Limitations Each device has a limited amount of high performance hardware decoders and encoders. If the maximum number is reached it will fall back to (slow) software de- and encoding. Therefore users may encounter slow export performance when trying to export in parallel with other software that utilizes encoders and decoders, e.g. Zoom, Teams or video content in other tabs / apps. This, unfortunately, is a limitation of hardware, operating system and browser and cannot be solved. ## Targets On Apple platforms, CE.SDK makes use of system-frameworks to benefit from hardware acceleration and platform native performance. The following targets are supported: - iOS & iPadOS 14 or later - macOS 12 or later ## Recommended Hardware - iPhone 8 or later - iPad (6th gen) or later - Macs released in the last 7 years ## Video Playback and exporting is **supported for all codecs** mentioned in the general section. However, mobile devices have stricter limits around the number of parallel encoders and decoders compared to fully fledged desktop machines. This means, that very large scenes with more than 10 videos shown in parallel may fail to play all videos at the same time and can’t be exported. ## Export Limitations The export size is limited by the hardware capabilities of the device, e.g., due to the maximum texture size that can be allocated. The maximum possible export size can be queried via API, see [export guide](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/). --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Compatibility & Security" description: "Learn about CE.SDK's compatibility and security features." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/compatibility-fef719/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Compatibility & Security](https://img.ly/docs/cesdk/mac-catalyst/compatibility-fef719/) --- CE.SDK provides robust compatibility and security features across platforms. Learn about supported browsers, frameworks, file formats, language support, and how CE.SDK ensures secure operation in your applications. --- ## Related Pages - [System Compatibility](https://img.ly/docs/cesdk/mac-catalyst/compatibility-139ef9/) - Learn how device performance and hardware limits affect CE.SDK editing, rendering, and export capabilities. - [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/file-format-support-3c4b2a/) - See which image, video, audio, font, and template formats CE.SDK supports for import and export. - [Security](https://img.ly/docs/cesdk/mac-catalyst/security-777bfd/) - Learn how CE.SDK keeps your data private with client-side processing, secure licensing, and GDPR-compliant practices. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Concepts" description: "Key concepts and principles of CE.SDK" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) --- Key Concepts and principles of CE.SDK. --- ## Related Pages - [Key Concepts](https://img.ly/docs/cesdk/mac-catalyst/key-concepts-21a270/) - Explore CE.SDK’s key features—manual editing, automation, templates, AI tools, and full UI and API control. - [Key Capabilities](https://img.ly/docs/cesdk/mac-catalyst/key-capabilities-dbb5b1/) - Explore CE.SDK’s key features—manual editing, automation, templates, AI tools, and full UI and API control. - [Architecture](https://img.ly/docs/cesdk/mac-catalyst/concepts/architecture-6ea9b2/) - Understand how CE.SDK is structured around the CreativeEngine—the core runtime with six APIs for scenes, blocks, assets, events, variables, and editor state. - [Terminology](https://img.ly/docs/cesdk/mac-catalyst/concepts/terminology-99e82d/) - Definitions for the core terms and concepts used throughout CE.SDK documentation, including Engine, Scene, Block, Fill, Shape, Effect, and more. - [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) - Control editing access with Creator and Adopter roles, each offering tailored permissions and UI constraints. - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) - Learn how blocks define elements in a scene and how to structure them for rendering in CE.SDK. - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) - Create, configure, save, and load scenes—the root container for all design elements in CE.SDK. - [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) - Pages structure scenes in CE.SDK and must share the same dimensions to ensure consistent rendering. - [Exclusion Areas](https://img.ly/docs/cesdk/mac-catalyst/concepts/exclusion-areas-c2b87b/) - Mark a region of a page that content must stay out of, and cut it out of an export. - [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) - Learn how assets provide external content to CE.SDK designs and how asset sources make them available programmatically. - [Editor State](https://img.ly/docs/cesdk/mac-catalyst/concepts/edit-modes-1f5b6c/) - Control how users interact with content by switching between edit modes like transform, crop, and text. - [Templating](https://img.ly/docs/cesdk/mac-catalyst/concepts/templating-f94385/) - Understand how templates work in CE.SDK—reusable designs with variables for dynamic text and placeholders for swappable media. - [Import and Export](https://img.ly/docs/cesdk/mac-catalyst/concepts/import-export-0ebbd8/) - Understand every way files move in and out of CE.SDK—importing media and design files, loading and saving native scenes, and exporting finished output. - [Events](https://img.ly/docs/cesdk/mac-catalyst/concepts/events-353f97/) - Subscribe to block creation, update, and deletion events to track changes in your CE.SDK scene. - [Buffers](https://img.ly/docs/cesdk/mac-catalyst/concepts/buffers-9c565b/) - Use buffers to store temporary, non-serializable data in CE.SDK via the CreativeEngine API. - [Working With Resources](https://img.ly/docs/cesdk/mac-catalyst/concepts/resources-a58d71/) - Preload resources, find transient data, detect MIME types, and relocate URLs in CE.SDK for Swift. - [Cancel Operations](https://img.ly/docs/cesdk/mac-catalyst/concepts/cancel-operations-70cf0e/) - Cancel an export, a load or a save by cancelling its task, give a call a deadline, and learn what the engine keeps after a cancel. - [Undo and History](https://img.ly/docs/cesdk/mac-catalyst/concepts/undo-and-history-99479d/) - Manage undo and redo stacks in CE.SDK using multiple histories, callbacks, and API-based controls. - [Design Units](https://img.ly/docs/cesdk/mac-catalyst/concepts/design-units-cc6597/) - Configure design units (pixels, millimeters, inches) and DPI settings for print-ready output in CE.SDK. - [Font Size Unit](https://img.ly/docs/cesdk/mac-catalyst/concepts/font-size-unit-3b2d60/) - Configure how font sizes are interpreted (Point vs Pixel) per scene in the CE.SDK iOS engine. - [Export Counting](https://img.ly/docs/cesdk/mac-catalyst/export-counting-613923/) - Learn which operations count as an export in CE.SDK, when export events are recorded, and what data they contain. - [Error Catalog](https://img.ly/docs/cesdk/mac-catalyst/concepts/error-catalog-z3djzn/) - Reference of every structured CE.SDK engine error code, its message, hint, and related documentation page. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Architecture" description: "Understand how CE.SDK is structured around the CreativeEngine—the core runtime with six APIs for scenes, blocks, assets, events, variables, and editor state." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/architecture-6ea9b2/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Architecture](https://img.ly/docs/cesdk/mac-catalyst/concepts/architecture-6ea9b2/) --- ```swift file=@cesdk_swift_examples/engine-guides-concepts-architecture/Architecture.swift reference-only import Foundation import IMGLYEngine @MainActor func architecture(engine: Engine) async throws { // The engine exposes six API namespaces: _ = engine.scene // Scene API — content hierarchy _ = engine.block // Block API — create and modify blocks _ = engine.asset // Asset API — manage asset sources _ = engine.editor // Editor API — edit modes, undo/redo, roles _ = engine.event // Event API — subscribe to changes _ = engine.variable // Variable API — template variables // Create a scene with a page and a graphic block. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.appendChild(to: page, child: block) // Traverse the hierarchy. let pages = try engine.scene.getPages() let children = try engine.block.getChildren(pages.first!) _ = children // Design mode — static designs like social posts and print materials. let designScene = try engine.scene.create() // Video mode — time-based content with playback and timeline. let videoScene = try engine.scene.createVideo() _ = designScene _ = videoScene // Subscribe to block changes using AsyncStream. let subscription = engine.event.subscribe(to: [scene]) Task { for await events in subscription { for event in events { print("Block \(event.block) had event: \(event.type)") } } } // Set and retrieve template variables. try engine.variable.set(key: "username", value: "Jane") let username = try engine.variable.get(key: "username") _ = username } ``` Understand how CE.SDK is structured around the CreativeEngine and its six interconnected APIs. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-concepts-architecture) CE.SDK is built around the **CreativeEngine**—a single-threaded core runtime that manages state, rendering, and coordination between six specialized APIs. Understanding how these pieces connect helps you navigate the SDK effectively. ## The CreativeEngine The `Engine` is the central coordinator. All operations—creating content, manipulating blocks, rendering, and exporting—flow through it. Initialize it once and access everything else through its API namespaces. The `Engine` manages: - **One active scene** containing all design content - **Six API namespaces** for different domains of functionality - **Event dispatching** for reactive state management - **Resource loading** and caching - **Rendering** to a Metal view or offscreen context All engine operations run on the main thread. In Swift, this is enforced by marking the `Engine` class as `@MainActor`. ## Core APIs The engine exposes six API namespaces, each handling a specific domain of functionality: ```swift highlight-architecture-apis // The engine exposes six API namespaces: _ = engine.scene // Scene API — content hierarchy _ = engine.block // Block API — create and modify blocks _ = engine.asset // Asset API — manage asset sources _ = engine.editor // Editor API — edit modes, undo/redo, roles _ = engine.event // Event API — subscribe to changes _ = engine.variable // Variable API — template variables ``` | API | Namespace | Purpose | |-----|-----------|---------| | Scene API | `engine.scene` | Content hierarchy—create, load, save scenes | | Block API | `engine.block` | Create, modify, and query design blocks | | Asset API | `engine.asset` | Register and query asset sources | | Editor API | `engine.editor` | Edit modes, undo/redo, user roles | | Event API | `engine.event` | Subscribe to engine state changes | | Variable API | `engine.variable` | Template variables for data-driven designs | ## Content Hierarchy CE.SDK organizes content in a tree: **Scene** → **Pages** → **Blocks**. - **Scene**: The root container. One scene per engine instance. Operates in either *Design Mode* (static) or *Video Mode* (timeline-based). - **Pages**: Containers within a scene. Artboards in Design Mode, timeline compositions in Video Mode. - **Blocks**: The atomic units—graphics, text, audio, video. Everything visible is a block. Create a scene, add a page, and populate it with blocks: ```swift highlight-architecture-hierarchy // Create a scene with a page and a graphic block. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.appendChild(to: page, child: block) // Traverse the hierarchy. let pages = try engine.scene.getPages() let children = try engine.block.getChildren(pages.first!) ``` The **Scene API** manages this hierarchy. The **Block API** manipulates individual blocks within it. See [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) and [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) for details. ## Scene Modes CE.SDK supports two scene modes that determine available features and behavior: ```swift highlight-architecture-sceneModes // Design mode — static designs like social posts and print materials. let designScene = try engine.scene.create() // Video mode — time-based content with playback and timeline. let videoScene = try engine.scene.createVideo() ``` - **Design Mode**: Static designs—social posts, print materials, graphics. Blocks are positioned spatially on pages. Created with `engine.scene.create()`. - **Video Mode**: Time-based content with playback, timeline, and audio support. Blocks have temporal properties like duration and trim. Created with `engine.scene.createVideo()`. Choose the mode when creating a scene. It determines which Block API properties and Editor API capabilities are available. See [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) for details. ## Event System Subscribe to engine events to build reactive UIs that update when state changes. The Event API provides Swift-native `AsyncStream` for consuming events: ```swift highlight-architecture-events // Subscribe to block changes using AsyncStream. let subscription = engine.event.subscribe(to: [scene]) Task { for await events in subscription { for event in events { print("Block \(event.block) had event: \(event.type)") } } } ``` Store your `Task` and cancel it when you no longer need updates to prevent leaks. See [Events](https://img.ly/docs/cesdk/mac-catalyst/concepts/events-353f97/) for details on subscribing to engine state changes. ## Template Variables The Variable API enables data-driven designs. Define variables at the scene level and reference them in text blocks with `{{variableName}}` syntax: ```swift highlight-architecture-variables // Set and retrieve template variables. try engine.variable.set(key: "username", value: "Jane") let username = try engine.variable.get(key: "username") ``` When variable values change, affected blocks update automatically. ## How They Connect A typical flow shows the interconnection: 1. **Scene API** creates the content structure 2. **Asset API** provides images, templates, or other content 3. **Block API** creates blocks and applies assets to them 4. **Variable API** injects dynamic data into text blocks 5. **Editor API** controls what users can modify 6. **Event API** notifies your UI of every change Each API focuses on one domain but works through the others. The Engine coordinates these interactions. ## Integration Patterns CE.SDK runs in two contexts on Apple platforms, determined by the render context you choose at initialization: - **Interactive**: Pass a Metal view as the render context. The engine renders content on screen in real time. Use the built-in editor UI (`IMGLYEditor`) for a full editing experience on iOS, or build your own SwiftUI interface on top of the engine APIs for complete control. - **Headless**: Initialize with an `.offscreen` render context—no view required. Use for server-side exports, automation, and batch operations where you need to process designs without displaying them. Both patterns use the same six APIs—only rendering differs. ## Next Steps - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) — Scene creation and management - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Working with design blocks --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Assets" description: "Learn how assets provide external content to CE.SDK designs and how asset sources make them available programmatically." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) --- Understand the asset system—how external media and resources like images, stickers, or videos are handled in CE.SDK. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-concepts-assets) Images, videos, audio, fonts, stickers, and templates—every premade resource you can add to a design is what we call an *Asset*. The editor gets access to these Assets through *Asset Sources*. When you apply an Asset, CE.SDK creates or modifies a Block to display that content. ```swift file=@cesdk_swift_examples/engine-guides-concepts-assets/ConceptsAssets.swift reference-only import Foundation import IMGLYEngine // MARK: - Custom Asset Source class DemoAssetSource: NSObject, AssetSource { let id = "my-assets" var supportedMIMETypes: [String]? { nil } var credits: AssetCredits? { nil } var license: AssetLicense? { nil } // Base URL the sample sticker is resolved against. let baseURL: URL init(baseURL: URL) { self.baseURL = baseURL super.init() } var stickerAsset: AssetResult { let stickerURI = baseURL .appendingPathComponent("ly.img.sticker/images/emoticons/imgly_sticker_emoticons_smile.svg") .absoluteString return AssetResult( id: "sticker-smile", label: "Smile Sticker", tags: ["emoji", "happy"], meta: [ "uri": stickerURI, "thumbUri": stickerURI, "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "width": "62", "height": "58", "mimeType": "image/svg+xml", ], context: AssetContext(sourceID: "my-assets"), groups: ["stickers"], ) } func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { AssetQueryResult( assets: [stickerAsset], currentPage: queryData.page, nextPage: -1, total: 1, ) } } // MARK: - Guide @MainActor func conceptsAssets(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let baseURL = try engine.guidesBaseURL // Register a custom asset source let source = DemoAssetSource(baseURL: baseURL) try engine.asset.addSource(source) // Query assets from a registered source let results = try await engine.asset.findAssets( sourceID: "my-assets", query: .init(query: nil, page: 0, perPage: 10), ) print("Found assets:", results.total) // Apply an asset to create a block in the scene if let asset = results.assets.first { let blockID = try await engine.asset.apply(sourceID: "my-assets", assetResult: asset) print("Created block:", blockID as Any) } // Local sources store assets in memory and support dynamic add/remove try engine.asset.addLocalSource(sourceID: "uploads", supportedMimeTypes: ["image/svg+xml", "image/png"]) let uploadedStickerURI = baseURL .appendingPathComponent("ly.img.sticker/images/emoticons/imgly_sticker_emoticons_grin.svg") .absoluteString try engine.asset.addAsset( to: "uploads", asset: AssetDefinition( id: "uploaded-1", meta: [ "uri": uploadedStickerURI, "thumbUri": uploadedStickerURI, "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "mimeType": "image/svg+xml", ], label: ["en": "Grin Sticker"], ), ) // Subscribe to asset source lifecycle events let task = Task { for await sourceID in engine.asset.onAssetSourceUpdated { print("Source updated:", sourceID) break } } // Notify that source contents changed try engine.asset.assetSourceContentsChanged(sourceID: "uploads") task.cancel() } ``` This guide covers the core concepts of the Asset system. For detailed instructions on inserting images, see the [Images](https://img.ly/docs/cesdk/mac-catalyst/insert-media/images-63848a/) guide. For related concepts, see [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) and [Resources](https://img.ly/docs/cesdk/mac-catalyst/concepts/resources-a58d71/). ## Assets vs Blocks **Assets** are content definitions with metadata (URIs, dimensions, labels) that exist outside the scene. **Blocks** are the visual elements in the scene tree that display content. When you apply an asset, CE.SDK creates a block configured according to the asset's properties. Multiple blocks can reference the same asset, and assets can exist without being used in any block. ## The Asset Data Model An asset describes content that can be added to designs. Each asset has an `id` and optional properties: ```swift highlight-conceptsAssets-assetDefinition var stickerAsset: AssetResult { let stickerURI = baseURL .appendingPathComponent("ly.img.sticker/images/emoticons/imgly_sticker_emoticons_smile.svg") .absoluteString return AssetResult( id: "sticker-smile", label: "Smile Sticker", tags: ["emoji", "happy"], meta: [ "uri": stickerURI, "thumbUri": stickerURI, "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "width": "62", "height": "58", "mimeType": "image/svg+xml", ], context: AssetContext(sourceID: "my-assets"), groups: ["stickers"], ) } ``` Key properties include: - **`id`** — Unique identifier for the asset - **`label`** — Display name (can be localized) - **`tags`** — Searchable keywords - **`groups`** — Categories for filtering - **`meta`** — Content-specific data including `uri`, `thumbUri`, `blockType`, `fillType`, `width`, `height`, and `mimeType` - **`context`** — An `AssetContext` that ties the asset to its source > **Note:** See the [Content JSON Schema](https://img.ly/docs/cesdk/mac-catalyst/import-media/content-json-schema-a7b3d2/) guide for the complete property reference. ## Asset Sources Asset sources provide assets to the editor. Each source conforms to the `AssetSource` protocol and implements a `findAssets(queryData:)` method that returns paginated results. ```swift highlight-conceptsAssets-assetSource func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { AssetQueryResult( assets: [stickerAsset], currentPage: queryData.page, nextPage: -1, total: 1, ) } ``` The `findAssets(queryData:)` callback receives an `AssetQueryData` with parameters like `page`, `perPage`, `query`, `tags`, `groups`, `filter`, and `facets`, and returns an `AssetQueryResult` with `assets`, `total`, `currentPage`, and `nextPage`, plus a `facets` dictionary when the query requests distributions. Sources can also implement optional methods like `getGroups()`, `fetchAsset(id:options:)`, and `apply(asset:)` for custom behavior. ## Querying Assets Search and filter assets from registered sources using `findAssets(sourceID:query:)`: ```swift highlight-conceptsAssets-queryAssets // Query assets from a registered source let results = try await engine.asset.findAssets( sourceID: "my-assets", query: .init(query: nil, page: 0, perPage: 10), ) print("Found assets:", results.total) ``` Results include pagination info. Loop through pages until `nextPage` is `-1` to retrieve all matching assets. The optional `filter` parameter narrows a query with structured `AssetFilter` predicates (`equals` and `contains` on a property path, combined with `and`, `or`, and `not`). The optional `facets` parameter requests value distributions for `tags`, `groups`, or `meta.` paths over the matched set—for example to populate a filter dropdown. Each distribution is ordered by count descending and returned in `AssetQueryResult.facets`; combine `facets` with `perPage: 0` to enumerate available values without fetching assets. ## Applying Assets Use `apply(sourceID:assetResult:)` to create a new block from an asset: ```swift highlight-conceptsAssets-applyAsset // Apply an asset to create a block in the scene if let asset = results.assets.first { let blockID = try await engine.asset.apply(sourceID: "my-assets", assetResult: asset) print("Created block:", blockID as Any) } ``` The method returns the new block ID, which you can use to position and configure the block. ## Local Asset Sources Local sources store assets in memory and support dynamic add/remove operations. Use these for user uploads or runtime-generated content: ```swift highlight-conceptsAssets-localSource // Local sources store assets in memory and support dynamic add/remove try engine.asset.addLocalSource(sourceID: "uploads", supportedMimeTypes: ["image/svg+xml", "image/png"]) let uploadedStickerURI = baseURL .appendingPathComponent("ly.img.sticker/images/emoticons/imgly_sticker_emoticons_grin.svg") .absoluteString try engine.asset.addAsset( to: "uploads", asset: AssetDefinition( id: "uploaded-1", meta: [ "uri": uploadedStickerURI, "thumbUri": uploadedStickerURI, "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "mimeType": "image/svg+xml", ], label: ["en": "Grin Sticker"], ), ) ``` ## Source Events Subscribe to asset source lifecycle events for reactive UIs: ```swift highlight-conceptsAssets-sourceEvents // Subscribe to asset source lifecycle events let task = Task { for await sourceID in engine.asset.onAssetSourceUpdated { print("Source updated:", sourceID) break } } // Notify that source contents changed try engine.asset.assetSourceContentsChanged(sourceID: "uploads") task.cancel() ``` Call `assetSourceContentsChanged(sourceID:)` after modifying a source to notify subscribers. ## Next Steps - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Learn how applied assets become blocks in the scene tree. - [Resources](https://img.ly/docs/cesdk/mac-catalyst/concepts/resources-a58d71/) — Understand how URIs and buffers back asset content. - [Content JSON Schema](https://img.ly/docs/cesdk/mac-catalyst/import-media/content-json-schema-a7b3d2/) — Full reference for the asset property schema. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Blocks" description: "Learn how blocks define elements in a scene and how to structure them for rendering in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) --- ```swift file=@cesdk_swift_examples/engine-guides-concepts-blocks/ConceptsBlocks.swift reference-only import Foundation import IMGLYEngine @MainActor func conceptsBlocks(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) let baseURL = try engine.guidesBaseURL // Find the page block - pages contain all design elements let pages = try engine.block.find(byType: .page) let firstPage = pages[0] // Query the block type - returns the full type path let pageType = try engine.block.getType(firstPage) print("Page block type:", pageType) // '//ly.img.ubq/page' // Type is immutable, determined at creation // Kind is a custom label you can set and change try engine.block.setKind(firstPage, kind: "main-canvas") let pageKind = try engine.block.getKind(firstPage) print("Page kind:", pageKind) // 'main-canvas' // Find blocks by kind let mainCanvasBlocks = try engine.block.find(byKind: "main-canvas") print("Blocks with kind 'main-canvas':", mainCanvasBlocks.count) // Create a graphic block for an image let graphic = try engine.block.create(.graphic) // Duplicate creates a copy with a new UUID let graphicCopy = try engine.block.duplicate(graphic) // Destroy removes a block - the duplicate is no longer needed try engine.block.destroy(graphicCopy) // Check if a block ID is still valid after operations let isOriginalValid = engine.block.isValid(graphic) let isCopyValid = engine.block.isValid(graphicCopy) print("Original valid:", isOriginalValid) // true print("Copy valid after destroy:", isCopyValid) // false // Create a rect shape to define the graphic's bounds let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(graphic, shape: rectShape) // Position and size the graphic try engine.block.setPositionX(graphic, value: 200) try engine.block.setPositionY(graphic, value: 100) try engine.block.setWidth(graphic, value: 400) try engine.block.setHeight(graphic, value: 300) // Create an image fill and attach it to the graphic let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(graphic, fill: imageFill) // Set content fill mode so the image fills the block bounds try engine.block.setEnum(graphic, property: "contentFill/mode", value: "Cover") // Blocks form a tree: scene > page > elements // Append the graphic to the page to make it visible try engine.block.appendChild(to: page, child: graphic) // Query parent-child relationships let graphicParent = try engine.block.getParent(graphic) print("Graphic parent is page:", graphicParent == page) // true let pageChildren = try engine.block.getChildren(page) print("Page has children:", pageChildren.count) // Create a text block with content let textBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: textBlock) // Position the text block try engine.block.setPositionX(textBlock, value: 200) try engine.block.setPositionY(textBlock, value: 450) try engine.block.setWidth(textBlock, value: 400) try engine.block.setHeight(textBlock, value: 80) // Set text content and styling try engine.block.setString(textBlock, property: "text/text", value: "Blocks are the building units of CE.SDK designs") try engine.block.setFloat(textBlock, property: "text/fontSize", value: 24) try engine.block.setEnum(textBlock, property: "text/horizontalAlignment", value: "Center") // Check the text block type let textType = try engine.block.getType(textBlock) print("Text block type:", textType) // '//ly.img.ubq/text' // Use reflection to discover available properties let graphicProperties = try engine.block.findAllProperties(graphic) print("Graphic block has", graphicProperties.count, "properties") // Get property type information let opacityType = try engine.block.getType(ofProperty: "opacity") print("Opacity property type:", opacityType) // .float // Check if properties are readable/writable let isOpacityReadable = try engine.block.isPropertyReadable(property: "opacity") let isOpacityWritable = try engine.block.isPropertyWritable(property: "opacity") print("Opacity readable:", isOpacityReadable, "writable:", isOpacityWritable) // Use type-specific getters and setters // Float properties try engine.block.setFloat(graphic, property: "opacity", value: 0.9) let opacity = try engine.block.getFloat(graphic, property: "opacity") print("Graphic opacity:", opacity) // Bool properties try engine.block.setBool(page, property: "page/marginEnabled", value: false) let marginEnabled = try engine.block.getBool(page, property: "page/marginEnabled") print("Page margin enabled:", marginEnabled) // Enum properties - get allowed values first let blendModes = try engine.block.getEnumValues(ofProperty: "blend/mode") print("Available blend modes:", blendModes.prefix(3).joined(separator: ", "), "...") try engine.block.setEnum(graphic, property: "blend/mode", value: "Multiply") let blendMode = try engine.block.getEnum(graphic, property: "blend/mode") print("Graphic blend mode:", blendMode) // Vec2 properties hold two coordinates, such as the position of a block try engine.block.setVec2(graphic, property: "position", value: SIMD2(200, 100)) let position = try engine.block.getVec2(graphic, property: "position") print("Graphic position:", position.x, position.y) // Each block has a stable UUID across save/load cycles let graphicUUID = try engine.block.getUUID(graphic) print("Graphic UUID:", graphicUUID) // Block names are mutable labels for organization try engine.block.setName(graphic, name: "Hero Image") try engine.block.setName(textBlock, name: "Caption") let graphicName = try engine.block.getName(graphic) print("Graphic name:", graphicName) // 'Hero Image' // Select a block programmatically try engine.block.select(graphic) // Selects graphic, deselects others // Check selection state let isGraphicSelected = try engine.block.isSelected(graphic) print("Graphic is selected:", isGraphicSelected) // true // Add to selection without deselecting others try engine.block.setSelected(textBlock, selected: true) // Get all selected blocks let selectedBlocks = engine.block.findAllSelected() print("Selected blocks count:", selectedBlocks.count) // 2 // Subscribe to selection changes let selectionTask = Task { for await _ in engine.block.onSelectionChanged { let selected = engine.block.findAllSelected() print("Selection changed, now selected:", selected.count, "blocks") } } // Control block visibility try engine.block.setVisible(graphic, visible: true) let isVisible = try engine.block.isVisible(graphic) print("Graphic is visible:", isVisible) // Control export inclusion try engine.block.setIncludedInExport(graphic, enabled: true) let inExport = try engine.block.isIncludedInExport(graphic) print("Graphic included in export:", inExport) // Control clipping behavior try engine.block.setClipped(graphic, clipped: false) let isClipped = try engine.block.isClipped(graphic) print("Graphic is clipped:", isClipped) // Query block state - indicates loading status let graphicState = try engine.block.getState(graphic) print("Graphic state:", graphicState) // An error state carries the engine error behind it when the engine knows it if case let .error(_, engineError?) = graphicState { print(engineError.code, engineError.message) } // Subscribe to state changes (useful for loading indicators) let stateTask = Task { for await changedBlocks in engine.block.onStateChanged([graphic]) { for blockID in changedBlocks { let state = try engine.block.getState(blockID) print("Block \(blockID) state changed to:", state) } } } // Save blocks to a string for persistence let savedString = try await engine.block.saveToString(blocks: [graphic, textBlock]) print("Blocks saved to string, length:", savedString.count) // Load blocks from string (creates new blocks, not attached to scene) let loadedBlocks = try await engine.block.load(from: savedString) print("Loaded blocks from string:", loadedBlocks.count) // Loaded blocks must be parented to appear in the scene // For demo purposes, destroy them to avoid duplicates for loadedBlock in loadedBlocks { try engine.block.destroy(loadedBlock) } // Clean up async subscriptions selectionTask.cancel() stateTask.cancel() } ``` Work with blocks—the fundamental building units for all visual elements in CE.SDK designs. > **Reading time:** 15 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-concepts-blocks) Every visual element in CE.SDK—images, text, shapes, and audio—is represented as a block. Blocks are organized in a tree structure within scenes and pages, where parent-child relationships determine rendering order and visibility. Each block has properties you can read and modify, a `Type` that defines its core behavior, and an optional `Kind` for custom categorization. This guide covers block types and their uses, how to create and manage blocks programmatically, how to work with block properties using the reflection system, and how to handle selection, visibility, and state changes. ## Block Types CE.SDK provides several block types, each designed for specific content: - **graphic** (`//ly.img.ubq/graphic`): Visual blocks for images, shapes, and graphics - **text** (`//ly.img.ubq/text`): Text content with typography controls - **audio** (`//ly.img.ubq/audio`): Audio content for video scenes - **page** (`//ly.img.ubq/page`): Container blocks representing canvases or artboards - **cutout** (`//ly.img.ubq/cutout`): Blocks for masking operations Query a block's type using `getType()` and find blocks of a specific type with `find(byType:)`: ```swift highlight-block-types // Find the page block - pages contain all design elements let pages = try engine.block.find(byType: .page) let firstPage = pages[0] // Query the block type - returns the full type path let pageType = try engine.block.getType(firstPage) print("Page block type:", pageType) // '//ly.img.ubq/page' ``` Block types are immutable—once created, a block's type cannot change. This distinguishes type from kind. ## Type vs Kind Type and kind serve different purposes. The **type** is determined at creation and defines core behavior. The **kind** is a custom string label you assign for application-specific categorization. ```swift highlight-type-vs-kind // Type is immutable, determined at creation // Kind is a custom label you can set and change try engine.block.setKind(firstPage, kind: "main-canvas") let pageKind = try engine.block.getKind(firstPage) print("Page kind:", pageKind) // 'main-canvas' // Find blocks by kind let mainCanvasBlocks = try engine.block.find(byKind: "main-canvas") print("Blocks with kind 'main-canvas':", mainCanvasBlocks.count) ``` Use kind to tag blocks for your application's logic. Set it with `setKind()`, query it with `getKind()`, and find blocks by kind with `find(byKind:)`. ## Block Hierarchy Blocks form a tree structure where scenes contain pages, and pages contain design elements. ```swift highlight-block-hierarchy // Blocks form a tree: scene > page > elements // Append the graphic to the page to make it visible try engine.block.appendChild(to: page, child: graphic) // Query parent-child relationships let graphicParent = try engine.block.getParent(graphic) print("Graphic parent is page:", graphicParent == page) // true let pageChildren = try engine.block.getChildren(page) print("Page has children:", pageChildren.count) ``` Only blocks that are direct or indirect children of a page block are rendered. A scene without any page children won't display content in the editor. Use `appendChild(to:child:)` to add blocks to parents, `getParent()` to query a block's parent, and `getChildren()` to get a block's children. ## Block Lifecycle Create new blocks with `create()`, duplicate existing blocks with `duplicate()`, and remove blocks with `destroy()`. After destroying a block, `isValid()` returns `false` for that block ID. ```swift highlight-block-lifecycle // Create a graphic block for an image let graphic = try engine.block.create(.graphic) // Duplicate creates a copy with a new UUID let graphicCopy = try engine.block.duplicate(graphic) // Destroy removes a block - the duplicate is no longer needed try engine.block.destroy(graphicCopy) // Check if a block ID is still valid after operations let isOriginalValid = engine.block.isValid(graphic) let isCopyValid = engine.block.isValid(graphicCopy) print("Original valid:", isOriginalValid) // true print("Copy valid after destroy:", isCopyValid) // false ``` When duplicating a block, all children are included, and the duplicate receives a new UUID. ## Working with Fills Graphic blocks display content through fills. Create a fill, attach it to a block, and configure its source. ```swift highlight-fill // Create a rect shape to define the graphic's bounds let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(graphic, shape: rectShape) // Position and size the graphic try engine.block.setPositionX(graphic, value: 200) try engine.block.setPositionY(graphic, value: 100) try engine.block.setWidth(graphic, value: 400) try engine.block.setHeight(graphic, value: 300) // Create an image fill and attach it to the graphic let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(graphic, fill: imageFill) // Set content fill mode so the image fills the block bounds try engine.block.setEnum(graphic, property: "contentFill/mode", value: "Cover") ``` CE.SDK supports several fill types including image, video, color, and gradient fills. See the [Fills guide](https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/) for details on available fill types. ## Creating Text Blocks Text blocks display formatted text content. Create a text block, position it, and set its content and styling. ```swift highlight-text-block // Create a text block with content let textBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: textBlock) // Position the text block try engine.block.setPositionX(textBlock, value: 200) try engine.block.setPositionY(textBlock, value: 450) try engine.block.setWidth(textBlock, value: 400) try engine.block.setHeight(textBlock, value: 80) // Set text content and styling try engine.block.setString(textBlock, property: "text/text", value: "Blocks are the building units of CE.SDK designs") try engine.block.setFloat(textBlock, property: "text/fontSize", value: 24) try engine.block.setEnum(textBlock, property: "text/horizontalAlignment", value: "Center") // Check the text block type let textType = try engine.block.getType(textBlock) print("Text block type:", textType) // '//ly.img.ubq/text' ``` Text blocks support extensive typography controls covered in the [Text guides](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/). ## Block Properties The reflection system lets you discover and manipulate any block property dynamically. Use `findAllProperties()` to get all available properties for a block—they're prefixed by category like `shape/star/points` or `text/fontSize`. ```swift highlight-block-properties // Use reflection to discover available properties let graphicProperties = try engine.block.findAllProperties(graphic) print("Graphic block has", graphicProperties.count, "properties") // Get property type information let opacityType = try engine.block.getType(ofProperty: "opacity") print("Opacity property type:", opacityType) // .float // Check if properties are readable/writable let isOpacityReadable = try engine.block.isPropertyReadable(property: "opacity") let isOpacityWritable = try engine.block.isPropertyWritable(property: "opacity") print("Opacity readable:", isOpacityReadable, "writable:", isOpacityWritable) ``` Query property types with `getType(ofProperty:)`. Returns a `PropertyType` value such as `.bool`, `.int`, `.float`, `.double`, `.string`, `.color`, `.enum`, `.vec2`, or `.struct`. For enum properties, use `getEnumValues(ofProperty:)` to get allowed values. ### Property Accessors Use type-specific getters and setters matching the property type: ```swift highlight-property-accessors // Use type-specific getters and setters // Float properties try engine.block.setFloat(graphic, property: "opacity", value: 0.9) let opacity = try engine.block.getFloat(graphic, property: "opacity") print("Graphic opacity:", opacity) // Bool properties try engine.block.setBool(page, property: "page/marginEnabled", value: false) let marginEnabled = try engine.block.getBool(page, property: "page/marginEnabled") print("Page margin enabled:", marginEnabled) // Enum properties - get allowed values first let blendModes = try engine.block.getEnumValues(ofProperty: "blend/mode") print("Available blend modes:", blendModes.prefix(3).joined(separator: ", "), "...") try engine.block.setEnum(graphic, property: "blend/mode", value: "Multiply") let blendMode = try engine.block.getEnum(graphic, property: "blend/mode") print("Graphic blend mode:", blendMode) // Vec2 properties hold two coordinates, such as the position of a block try engine.block.setVec2(graphic, property: "position", value: SIMD2(200, 100)) let position = try engine.block.getVec2(graphic, property: "position") print("Graphic position:", position.x, position.y) ``` Using the wrong accessor type for a property causes an error. Always check `getType(ofProperty:)` if you're unsure which accessor to use. > **Caution:** Experimental: these APIs may change in a minor release. A `.vec2` property holds two coordinates as a `SIMD2`. `position` and `shape/position` are `.vec2` properties: `setVec2(_:property:value:)` writes `position/x` and `position/y` together, and a failed write changes neither coordinate. ## UUID, Names, and Identity Each block has a UUID that remains stable across save and load operations. Block names are mutable labels for organization. ```swift highlight-uuid-identity // Each block has a stable UUID across save/load cycles let graphicUUID = try engine.block.getUUID(graphic) print("Graphic UUID:", graphicUUID) // Block names are mutable labels for organization try engine.block.setName(graphic, name: "Hero Image") try engine.block.setName(textBlock, name: "Caption") let graphicName = try engine.block.getName(graphic) print("Graphic name:", graphicName) // 'Hero Image' ``` Use `getUUID()` when you need a persistent identifier for a block. Names are useful for user-facing labels and can be changed freely with `setName()`. ## Selection Control which blocks are selected programmatically. Use `select()` to select a single block (deselecting others) or `setSelected()` to modify selection without affecting other blocks. ```swift highlight-selection // Select a block programmatically try engine.block.select(graphic) // Selects graphic, deselects others // Check selection state let isGraphicSelected = try engine.block.isSelected(graphic) print("Graphic is selected:", isGraphicSelected) // true // Add to selection without deselecting others try engine.block.setSelected(textBlock, selected: true) // Get all selected blocks let selectedBlocks = engine.block.findAllSelected() print("Selected blocks count:", selectedBlocks.count) // 2 // Subscribe to selection changes let selectionTask = Task { for await _ in engine.block.onSelectionChanged { let selected = engine.block.findAllSelected() print("Selection changed, now selected:", selected.count, "blocks") } } ``` Subscribe to selection changes with `onSelectionChanged`, an `AsyncStream` you iterate over with `for await` to update your UI when the selection state changes. ## Visibility Control whether blocks appear on the canvas and are included in exports. ```swift highlight-visibility // Control block visibility try engine.block.setVisible(graphic, visible: true) let isVisible = try engine.block.isVisible(graphic) print("Graphic is visible:", isVisible) // Control export inclusion try engine.block.setIncludedInExport(graphic, enabled: true) let inExport = try engine.block.isIncludedInExport(graphic) print("Graphic included in export:", inExport) ``` A block with `isVisible()` returning true may still not appear if it hasn't been added to a parent, the parent is hidden, or another block obscures it. ### Clipping Clipping determines whether a block's content is constrained to its parent's bounds. When `setClipped(block, clipped: true)` is set, any portion of the block extending beyond its parent's boundaries is hidden. When clipping is disabled, the block renders fully even if it overflows its parent container. ```swift highlight-clipping // Control clipping behavior try engine.block.setClipped(graphic, clipped: false) let isClipped = try engine.block.isClipped(graphic) print("Graphic is clipped:", isClipped) ``` ## Block State Blocks track loading progress and error conditions through a state system with three possible states: - **`.ready`**: Normal state, no pending operations - **`.pending(progress:)`**: Operation in progress with a progress value (0–1) - **`.error(_:engineError:)`**: Operation failed (`audioDecoding`, `imageDecoding`, `fileFetch`, `videoDecoding`, `unknown`). `engineError` is the `EngineError` behind the failure when the engine knows it, for example why a `.lottie` file cannot be read. Read `engineError` from the `.error` case to get the catalog `code` and the `message`. It is `nil` when the engine knows no catalog error, or when your code set the state with `setState(_:state:)`. A video that cannot be decoded, for example, gives the decoder's error. ```swift highlight-block-state // Query block state - indicates loading status let graphicState = try engine.block.getState(graphic) print("Graphic state:", graphicState) // An error state carries the engine error behind it when the engine knows it if case let .error(_, engineError?) = graphicState { print(engineError.code, engineError.message) } // Subscribe to state changes (useful for loading indicators) let stateTask = Task { for await changedBlocks in engine.block.onStateChanged([graphic]) { for blockID in changedBlocks { let state = try engine.block.getState(blockID) print("Block \(blockID) state changed to:", state) } } } ``` Subscribe to state changes with `onStateChanged()`, which returns an `AsyncStream` you iterate over to show loading indicators or handle errors in your UI. ## Serialization Save blocks to strings for persistence and restore them later. ```swift highlight-serialization // Save blocks to a string for persistence let savedString = try await engine.block.saveToString(blocks: [graphic, textBlock]) print("Blocks saved to string, length:", savedString.count) // Load blocks from string (creates new blocks, not attached to scene) let loadedBlocks = try await engine.block.load(from: savedString) print("Loaded blocks from string:", loadedBlocks.count) // Loaded blocks must be parented to appear in the scene // For demo purposes, destroy them to avoid duplicates for loadedBlock in loadedBlocks { try engine.block.destroy(loadedBlock) } ``` Use `saveToString(blocks:)` for lightweight serialization or `saveToArchive(blocks:)` to include all referenced assets. Blocks can be loaded with `load(from: String)`, `loadArchive(from: URL)`, or `load(from: URL)`. For `loadArchive(from:)`, the URL should point to the zipped archive file previously saved with `saveToArchive()`, whereas for `load(from: URL)`, it should point to a blocks file within an unzipped archive directory. Loaded blocks are not automatically attached to the scene—you must parent them with `appendChild(to:child:)` to make them visible. ## Troubleshooting **Block not visible**: Ensure the block is a child of a page that's a child of the scene. **Property setter fails**: Verify the property type matches the setter method used. Use `getType(ofProperty:)` to check. **Block ID invalid after destroy**: Use `isValid()` before operations on potentially destroyed blocks. **State stuck in Pending**: Check network connectivity for remote resources or use state change events to monitor progress. ## Next Steps - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) — Understand the root container that holds every block. - [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) — Learn how pages group blocks into discrete canvases. - [Events](https://img.ly/docs/cesdk/mac-catalyst/concepts/events-353f97/) — Subscribe to block creation, update, and deletion. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Buffers" description: "Use buffers to store temporary, non-serializable data in CE.SDK via the CreativeEngine API." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/buffers-9c565b/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Buffers](https://img.ly/docs/cesdk/mac-catalyst/concepts/buffers-9c565b/) --- ```swift file=@cesdk_swift_examples/engine-guides-buffers/Buffers.swift reference-only import Foundation import IMGLYEngine @MainActor func buffers(engine: Engine) throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) let audioBuffer = engine.editor.createBuffer() // Generate 10 seconds of stereo 48 kHz audio data let sampleCount = 10 * 2 * 48000 let samples = ContiguousArray(unsafeUninitializedCapacity: sampleCount) { buffer, initializedCount in for i in stride(from: 0, to: buffer.count, by: 2) { let sample = sin((440.0 * Float(i) * Float.pi) / 48000.0) buffer[i + 0] = sample buffer[i + 1] = sample } initializedCount = buffer.count } // Write the audio samples to the buffer try samples.withUnsafeBufferPointer { buffer in try engine.editor.setBufferData(url: audioBuffer, offset: 0, data: Data(buffer: buffer)) } // Read a subrange of the buffer data let chunk = try engine.editor.getBufferData(url: audioBuffer, offset: 0, length: 4096) // Query the current buffer length in bytes let length = try engine.editor.getBufferLength(url: audioBuffer) // Reduce the buffer to half its length, truncating from 10 to 5 seconds try engine.editor.setBufferLength(url: audioBuffer, length: UInt(truncating: length) / 2) // Create an audio block and assign the buffer as its source let audioBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: audioBlock) try engine.block.setURL(audioBlock, property: "audio/fileURI", value: audioBuffer) // Find all transient resources in the scene, including buffers let transientResources = try engine.editor.findAllTransientResources() for resource in transientResources { print("Transient resource: \(resource.url), size: \(resource.size) bytes") } // To persist buffer data, read it, upload to storage, then relocate let bufferData = try engine.editor.getBufferData( url: audioBuffer, offset: 0, length: UInt(truncating: try engine.editor.getBufferLength(url: audioBuffer)), ) // In production, upload `bufferData` to a CDN or cloud storage let persistentURL = URL(string: "https://example.com/audio/generated.raw")! // Update all references to the old buffer URI throughout the scene try engine.editor.relocateResource(currentURL: audioBuffer, relocatedURL: persistentURL) // Free buffer resources when no longer needed try engine.editor.destroyBuffer(url: audioBuffer) _ = chunk _ = bufferData } ``` Store and manage temporary binary data directly in memory using CE.SDK's buffer API for dynamically generated content like procedural audio or real-time image data. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-buffers) Buffers are in-memory containers for binary data referenced via `buffer://` URIs. Unlike external files that require network or file I/O, buffers exist only during the current session and are not serialized when saving scenes. This makes them ideal for procedural audio, real-time image data, or streaming content that doesn't need to persist beyond the current editing session. This guide covers how to create and manage buffers, write and read binary data, assign buffers to block properties like audio sources, and handle transient resources when saving scenes. ## Setting Up a Video Scene Since this example uses audio blocks, we first create a scene with a page. Audio blocks require a scene context with timeline support. ```swift highlight-buffers-setup let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) ``` ## Creating and Managing Buffers Use `engine.editor.createBuffer()` to allocate a new buffer and receive its URI. This URI follows the `buffer://` scheme and uniquely identifies the buffer within the engine instance. ```swift highlight-buffers-createBuffer let audioBuffer = engine.editor.createBuffer() ``` Buffers persist until you explicitly destroy them with `engine.editor.destroyBuffer()` or the engine instance is disposed. A buffer of 1 MiB or more moves to a temporary file, so it takes disk space instead of memory. For large buffers or long editing sessions, destroy buffers when they're no longer needed. ## Writing Data to Buffers Populate a buffer with binary data using `engine.editor.setBufferData()`. This method takes the buffer URL, an offset in bytes, and a `Data` value containing the bytes to write. In this example, we generate a 440 Hz sine wave as 10 seconds of stereo PCM audio samples at 48 kHz. We create a `ContiguousArray` for the sample values, then convert them to `Data` for writing. ```swift highlight-buffers-writeData // Generate 10 seconds of stereo 48 kHz audio data let sampleCount = 10 * 2 * 48000 let samples = ContiguousArray(unsafeUninitializedCapacity: sampleCount) { buffer, initializedCount in for i in stride(from: 0, to: buffer.count, by: 2) { let sample = sin((440.0 * Float(i) * Float.pi) / 48000.0) buffer[i + 0] = sample buffer[i + 1] = sample } initializedCount = buffer.count } // Write the audio samples to the buffer try samples.withUnsafeBufferPointer { buffer in try engine.editor.setBufferData(url: audioBuffer, offset: 0, data: Data(buffer: buffer)) } ``` The offset parameter supports incremental writes — you can append data or overwrite specific ranges within the buffer. ## Reading Data from Buffers Read data back from a buffer with `engine.editor.getBufferData()`, specifying the buffer URL, a starting offset, and the number of bytes to read. This returns a `Data` value that you can process or convert back to typed arrays. ```swift highlight-buffers-readData // Read a subrange of the buffer data let chunk = try engine.editor.getBufferData(url: audioBuffer, offset: 0, length: 4096) ``` Partial reads are supported — you can read any range within the buffer bounds. ## Querying Buffer Length Use `engine.editor.getBufferLength()` to determine the current size of a buffer in bytes. ```swift highlight-buffers-getLength // Query the current buffer length in bytes let length = try engine.editor.getBufferLength(url: audioBuffer) ``` ## Resizing Buffers Change a buffer's size with `engine.editor.setBufferLength()`. Increasing the size allocates additional space, while decreasing it truncates the data. Here we halve the buffer, reducing the audio from 10 to 5 seconds. ```swift highlight-buffers-resize // Reduce the buffer to half its length, truncating from 10 to 5 seconds try engine.editor.setBufferLength(url: audioBuffer, length: UInt(truncating: length) / 2) ``` Truncating a buffer permanently discards data beyond the new length. ## Assigning Buffers to Blocks Buffer URIs work like any other resource URI in CE.SDK. Assign them to block properties using `engine.block.setURL()`. For audio blocks, set the `audio/fileURI` property. ```swift highlight-buffers-assignBlock // Create an audio block and assign the buffer as its source let audioBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: audioBlock) try engine.block.setURL(audioBlock, property: "audio/fileURI", value: audioBuffer) ``` The same approach works for other resource properties: - **Audio blocks**: `audio/fileURI` - **Image fills**: `fill/image/imageFileURI` - **Video fills**: `fill/video/fileURI` Any property that accepts a URI can reference a buffer. ## Transient Resources and Scene Serialization Buffers are transient resources — the URI gets serialized when you save a scene, but the actual binary data does not persist. This means a saved scene will contain references to `buffer://` URIs that won't resolve when loaded again. Use `engine.editor.findAllTransientResources()` to discover all transient resources in the current scene, including buffers. Each resource includes its URL and size in bytes. ```swift highlight-buffers-transientResources // Find all transient resources in the scene, including buffers let transientResources = try engine.editor.findAllTransientResources() for resource in transientResources { print("Transient resource: \(resource.url), size: \(resource.size) bytes") } ``` > **Note:** **Limitations**Buffers are intended for temporary data only.* Buffer data is not part of [scene serialization](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) > * Changes to buffers can't be undone using the [history system](https://img.ly/docs/cesdk/mac-catalyst/concepts/undo-and-history-99479d/) ## Persisting Buffer Data To permanently save buffer content, extract the data, upload it to persistent storage, then use `engine.editor.relocateResource()` to update all references throughout the scene to point to the new URL. ```swift highlight-buffers-persistData // To persist buffer data, read it, upload to storage, then relocate let bufferData = try engine.editor.getBufferData( url: audioBuffer, offset: 0, length: UInt(truncating: try engine.editor.getBufferLength(url: audioBuffer)), ) // In production, upload `bufferData` to a CDN or cloud storage let persistentURL = URL(string: "https://example.com/audio/generated.raw")! // Update all references to the old buffer URI throughout the scene try engine.editor.relocateResource(currentURL: audioBuffer, relocatedURL: persistentURL) ``` After relocation, saving the scene serializes the new persistent URLs instead of the transient `buffer://` URIs. ## Troubleshooting **Buffer data not appearing in exported scene** Buffers are transient and don't persist with scene saves. Use `findAllTransientResources()` to identify buffers, then relocate them to persistent storage before exporting. **Memory usage growing unexpectedly** Call `engine.editor.destroyBuffer()` when buffers are no longer needed. Unlike external resources that can be garbage collected, buffers remain in memory until explicitly destroyed. **Data corruption when writing** Ensure the offset plus data length doesn't exceed the intended buffer bounds. Resize the buffer first with `setBufferLength()` if you need more space. **Buffer URI not recognized by block** Verify the buffer was created in the same engine instance. Buffer URIs are not portable between different engine instances or sessions. ## Next Steps - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) — Understand scene structure and serialization - [Undo and History](https://img.ly/docs/cesdk/mac-catalyst/concepts/undo-and-history-99479d/) — Learn about the history system and its limitations with buffers - [Resources](https://img.ly/docs/cesdk/mac-catalyst/concepts/resources-a58d71/) — Learn how CE.SDK manages resource URIs and loading --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Cancel Operations" description: "Cancel an export, a load or a save by cancelling its task, give a call a deadline, and learn what the engine keeps after a cancel." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/cancel-operations-70cf0e/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Cancel Operations](https://img.ly/docs/cesdk/mac-catalyst/concepts/cancel-operations-70cf0e/) --- ```swift file=@cesdk_swift_examples/engine-guides-cancel-operations/CancelOperations.swift reference-only import Foundation import IMGLYEngine @MainActor func cancelOperations(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL // Demo scaffolding: a page with an image block gives the exports below some content. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.setDuration(page, duration: 5) try engine.block.appendChild(to: scene, child: page) let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.setWidth(imageBlock, value: 800) try engine.block.setHeight(imageBlock, value: 600) try engine.block.appendChild(to: page, child: imageBlock) let exportTask = Task { try await engine.block.export(page, mimeType: .png) } exportTask.cancel() do { let blob = try await exportTask.value print("The export ended before the cancel with \(blob.count) bytes") } catch is CancellationError { print("The export stopped") } let blob = try await engine.block.export(page, mimeType: .png) print("The next export made a PNG file of \(blob.count) bytes") if let video = try await exportVideoUnlessSlow(engine: engine, page: page) { print("The video file has \(video.count) bytes") } else { print("The video export was too slow and stopped") } let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") let loadedInTime = try await withThrowingTaskGroup(of: Bool.self) { group in group.addTask { try await engine.scene.load(from: sceneURL) return true } group.addTask { try await Task.sleep(nanoseconds: 10 * NSEC_PER_SEC) return false } let first = try await group.next() ?? false group.cancelAll() return first } print(loadedInTime ? "The scene loaded in time" : "The scene did not load within 10 seconds") } @MainActor private func exportVideoUnlessSlow(engine: Engine, page: DesignBlockID) async throws -> Blob? { let startedAt = Date() for try await event in try await engine.block.exportVideo(page, mimeType: .mp4) { switch event { case let .progress(renderedFrames, _, totalFrames): if Date().timeIntervalSince(startedAt) > 60, renderedFrames < totalFrames / 2 { return nil } case let .finished(video: video): return video } } return nil } ``` Stop an export, a load or a save that your app no longer needs, or give it a deadline, by cancelling the Swift task that runs it. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-cancel-operations) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Exports, loads, downloads and saves can keep the engine busy long after you start them. When a user closes a screen or picks another file, your app no longer needs that work. You cancel a call by cancelling the Swift task that awaits it. The engine then ends the call, stops its work and frees what it holds. This guide covers how to cancel a call, how to start the next call and which calls you can cancel. It also shows how to give a call a deadline, how to stop a video export from its progress and what the engine keeps after a cancel. ## Cancelling a Call Start the call in a `Task` and keep the task. Call `cancel()` on the task to stop the call. Here we start a PNG export of a page that holds an image block, and cancel the export at once. ```swift highlight-cancelOperations-cancelExport let exportTask = Task { try await engine.block.export(page, mimeType: .png) } exportTask.cancel() do { let blob = try await exportTask.value print("The export ended before the cancel with \(blob.count) bytes") } catch is CancellationError { print("The export stopped") } ``` The task throws after the cancel, so the code after the call does not run. A cancelled call throws `CancellationError`, and the loop over an export stream, such as a video or audio export, ends without an error. The example catches `CancellationError` and lets every other error through. The task in the example has not started when `cancel()` runs. It still starts the export, which then ends at once. Check `Task.isCancelled` before a call when no work must start. A task that has already finished ignores the cancel, and `value` returns its result. You can call `cancel()` from any thread, because the cancel reaches the engine on the main actor by itself. A SwiftUI `.task` modifier cancels its task when the view goes away, so the calls that the task awaits stop with it. ## Starting the Next Call After the task throws, the engine is free for the next export or load. Start it after the `catch` or in a new task. Here we export the same page again, this time without a cancel. ```swift highlight-cancelOperations-exportAgain let blob = try await engine.block.export(page, mimeType: .png) print("The next export made a PNG file of \(blob.count) bytes") ``` Each image or PDF export runs on a worker engine of its own. The cancel releases the worker of the cancelled export, and the next export starts a new worker. ## Calls That Can Be Cancelled Each of these calls stops its engine work when you cancel the task that awaits it. - **Exports**: `engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:)` for images and PDF, `engine.block.export(_:to:mimeType:options:onPreExport:uriResolver:onProgress:)` for a PDF file, `engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:onData:)` for PDF chunks, `engine.block.exportWithColorMask(_:mimeType:maskColorR:maskColorG:maskColorB:options:onPreExport:uriResolver:)`, `engine.block.exportVideo(_:mimeType:options:onPreExport:uriResolver:)`, `engine.block.exportAudio(_:mimeType:options:)`, and their overloads that export several blocks - **Scene and block loads**: `engine.scene.load(from:overrideEditorConfig:waitForResources:onProgress:)` with a URL, `engine.scene.load(from:overrideEditorConfig:waitForResources:)` with a string and `waitForResources`, `engine.scene.loadArchive(from:overrideEditorConfig:waitForResources:onProgress:)`, `engine.scene.applyTemplate(from:)` with a URL, `engine.block.load(from:)`, `engine.block.loadArchive(from:)` - **New scenes from media**: `engine.scene.create(fromImage:dpi:pixelScaleFactor:sceneLayout:)`, `engine.scene.create(fromVideo:)` - **Downloads for a block or an asset**: `engine.block.createCaptionsFromURI(_:)`, `engine.block.addImageFileURIToSourceSet(_:property:uri:)`, `engine.block.addVideoFileURIToSourceSet(_:property:uri:)`, `engine.block.forceLoadAVResource(_:)`, `engine.block.forceLoadResources(_:)`, `engine.asset.addLocalAssetSourceFromJSON(_:matcher:)` with a URL, `engine.asset.apply(sourceID:assetResult:)`, `engine.asset.apply(sourceID:assetResult:placement:)`, `engine.asset.defaultApplyAsset(assetResult:)`, `engine.asset.defaultApplyAsset(assetResult:placement:)` - **Lookups**: `engine.editor.getMIMEType(url:)`, `engine.editor.getFontMetrics(fontFileURI:)`, `engine.block.getDominantColors(_:options:)` - **Color profiles**: `engine.scene.setDocumentColorProfile(_:from:)` with a URL, `engine.editor.loadColorProfiles()` - **Saves**: `engine.scene.saveToArchive(options:)`, `engine.block.saveToArchive(blocks:)`, and the saves to a string `engine.scene.saveToString(allowedResourceSchemes:onDisallowedResourceScheme:)`, `engine.scene.saveToString(options:)` and `engine.block.saveToString(blocks:allowedResourceSchemes:onDisallowedResourceScheme:)` while they wait for `onDisallowedResourceScheme` ## Giving a Call a Deadline No engine call takes a deadline. Run the call and a `Task.sleep(nanoseconds:)` in a task group, take the result that comes first, and cancel the other task. Here we give the load of the scene file at `sceneURL` ten seconds. ```swift highlight-cancelOperations-deadline let loadedInTime = try await withThrowingTaskGroup(of: Bool.self) { group in group.addTask { try await engine.scene.load(from: sceneURL) return true } group.addTask { try await Task.sleep(nanoseconds: 10 * NSEC_PER_SEC) return false } let first = try await group.next() ?? false group.cancelAll() return first } print(loadedInTime ? "The scene loaded in time" : "The scene did not load within 10 seconds") ``` When the sleep ends first, `cancelAll()` cancels the load, and the current scene stays. When the load ends first, `cancelAll()` ends the sleep. A load that fails before the deadline throws its error from `group.next()`, and the task group throws it again. From iOS 16, Mac Catalyst 16 and macOS 13 on, `Task.sleep(for: .seconds(10))` gives the same deadline. ## Cancelling a Video Export From Its Progress `engine.block.exportVideo(_:mimeType:options:onPreExport:uriResolver:)` returns an `AsyncThrowingStream` of `VideoExport` events. Leave the loop over the stream to stop the export. A `return` or a `break` ends the iteration, and the export stops and releases its worker engine. Here we stop a video export that has rendered less than half of its frames after one minute. ```swift highlight-cancelOperations-videoProgress @MainActor private func exportVideoUnlessSlow(engine: Engine, page: DesignBlockID) async throws -> Blob? { let startedAt = Date() for try await event in try await engine.block.exportVideo(page, mimeType: .mp4) { switch event { case let .progress(renderedFrames, _, totalFrames): if Date().timeIntervalSince(startedAt) > 60, renderedFrames < totalFrames / 2 { return nil } case let .finished(video: video): return video } } return nil } ``` A cancel of the task that runs the loop stops the export too. The loop then ends without an error, so the function returns `nil`. The first reports also count the start-up time of the export, so a projection of the total time from them would stop fast exports too. `engine.block.exportAudio(_:mimeType:options:)` returns a stream of `AudioExport` events, and leaving its loop stops it in the same way. ## What a Cancel Stops and What It Keeps A cancel stops the work of the call, but the engine keeps what other blocks and calls still need: - A video export stops at once and releases its worker engine with its memory. - An image or PDF export releases its worker engine. - An audio export that mixes a page stops at once and drops its mix. - An audio export that copies the audio of one block without the mixer finishes the copy in the background and drops it. - A load stops its download, unless a block or another call needs the same file. The engine reads every video fill at each update, so the download of a video fill goes on. - `load(from:)` and `loadArchive(from:)` keep the current scene. With `waitForResources` the new scene replaces it before the wait for its resources, so a cancel during that wait keeps the new scene. `load(from:)` with a string and `waitForResources` loads the scene at once, so a cancel ends only the wait. - `applyTemplate(from:)`, `create(fromImage:dpi:pixelScaleFactor:sceneLayout:)` and `create(fromVideo:)` keep the current scene. - `loadColorProfiles()` ends only the wait, because the document keeps its profile loaded. - A cancelled `setDocumentColorProfile(_:from:)` keeps the previous profile in that slot. An assignment that it replaced can still take effect. - An answer of `onDisallowedResourceScheme` after the cancel changes nothing. - `apply(sourceID:assetResult:)` stops the download that the default apply waits for, so it creates no block. An asset source with its own apply function keeps running in your code. A block that it creates stays, and the call does not return it. - `getDominantColors(_:options:)` waits while a resource of the block loads, so a cancel ends that wait. ## Troubleshooting - **The task returns a result although you cancelled it**: The call ended before the cancel. A task that has finished ignores `cancel()`. - **The call starts although you cancelled the task first**: A cancelled task still starts its call, which then ends at once. Check `Task.isCancelled` before the call. - **The download of a cancelled load goes on**: Another block or another call needs the same file. - **A video export goes on after you release the engine**: The export runs on a worker engine of its own. Cancel the task that runs the loop over the stream, or leave the loop. ## API Reference These are the methods this guide calls. The section Calls That Can Be Cancelled lists every method that stops when you cancel its task. | Method | Category | Purpose | | ------------------------------------------------------------------------------ | -------- | ---------------------------------------- | | `engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:)` | Block | Export a block as an image or a PDF | | `engine.block.exportVideo(_:mimeType:options:onPreExport:uriResolver:)` | Block | Export a page as a video | | `engine.scene.load(from:overrideEditorConfig:waitForResources:onProgress:)` | Scene | Load a scene or an archive from a URL | ## Next Steps - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) - Export designs and videos - [Resources](https://img.ly/docs/cesdk/mac-catalyst/concepts/resources-a58d71/) - Learn how the engine loads files - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) - Load and save scenes - [Error Catalog](https://img.ly/docs/cesdk/mac-catalyst/concepts/error-catalog-z3djzn/) - Look up engine error codes --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Design Units" description: "Configure design units (pixels, millimeters, inches) and DPI settings for print-ready output in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/design-units-cc6597/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Design Units](https://img.ly/docs/cesdk/mac-catalyst/concepts/design-units-cc6597/) --- ```swift file=@cesdk_swift_examples/engine-guides-design-units/DesignUnits.swift reference-only import Foundation import IMGLYEngine @MainActor func designUnits(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Get the current design unit — defaults to .px for new scenes let currentUnit = try engine.scene.getDesignUnit() print("Current design unit:", currentUnit) // .px // Switch to millimeters for a print workflow try engine.scene.setDesignUnit(.mm) // Verify the change let newUnit = try engine.scene.getDesignUnit() print("Design unit changed to:", newUnit) // .mm // Set DPI to 300 for print-quality exports try engine.block.setFloat(scene, property: "scene/dpi", value: 300) // Read back the DPI value let dpi = try engine.block.getFloat(scene, property: "scene/dpi") print("DPI set to:", dpi) // 300.0 // Set page to A4 dimensions (210 x 297 mm) try engine.block.setWidth(page, value: 210) try engine.block.setHeight(page, value: 297) let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) print("Page dimensions: \(pageWidth)mm x \(pageHeight)mm") // Create a text block positioned and sized in millimeters let textBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: textBlock) // Position at 20 mm from left, 30 mm from top try engine.block.setPositionX(textBlock, value: 20) try engine.block.setPositionY(textBlock, value: 30) // Size: 170 mm wide, 50 mm tall try engine.block.setWidth(textBlock, value: 170) try engine.block.setHeight(textBlock, value: 50) try engine.block.setString( textBlock, property: "text/text", value: "This A4 document uses millimeter units with 300 DPI for print-ready output.", ) // At 300 DPI: 1 inch = 300 pixels, 1 mm ≈ 11.81 pixels let a4WidthPixels = 210.0 * (300.0 / 25.4) let a4HeightPixels = 297.0 * (300.0 / 25.4) print("A4 at 300 DPI exports as \(Int(a4WidthPixels)) x \(Int(a4HeightPixels)) pixels") } ``` Control measurement systems for precise physical dimensions — create print-ready documents with millimeter or inch units and configurable DPI for export quality. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-design-units) Design units determine the coordinate system for all layout values in CE.SDK — positions, sizes, and margins. The engine supports three unit types: **Pixel** for screen-based designs, **Millimeter** for metric print dimensions, and **Inch** for imperial print formats. This guide covers how to get and set design units, configure DPI for export quality, and set up scenes for specific physical dimensions like A4 paper. ## Understanding Design Units ### Supported Unit Types CE.SDK supports three design unit types, each suited for different output scenarios: - **Pixel** (`.px`) — Default unit, ideal for screen-based designs, web graphics, and video content. One unit equals one pixel in the design coordinate space. - **Millimeter** (`.mm`) — For print designs targeting metric dimensions (A4, A5, business cards). One unit equals one millimeter at the scene's DPI setting. - **Inch** (`.in`) — For print designs targeting imperial dimensions (letter, legal, US business cards). One unit equals one inch at the scene's DPI setting. ### Design Unit and DPI Relationship DPI (dots per inch) determines how physical units convert to pixels during export. At 300 DPI, a 1-inch block exports as 300 pixels wide. Higher DPI values produce higher-resolution exports suitable for professional printing. For pixel-based scenes, DPI primarily affects font size conversions since font sizes are always specified in points. ## Getting the Current Design Unit Use `engine.scene.getDesignUnit()` to retrieve the current scene's design unit. This returns a `DesignUnit` enum value: `.px`, `.mm`, or `.in`. ```swift highlight-designUnits-getDesignUnit // Get the current design unit — defaults to .px for new scenes let currentUnit = try engine.scene.getDesignUnit() print("Current design unit:", currentUnit) // .px ``` ## Setting the Design Unit Use `engine.scene.setDesignUnit()` to change the measurement system. When you change the design unit, CE.SDK automatically converts existing layout values to maintain visual appearance. ```swift highlight-designUnits-setDesignUnit // Switch to millimeters for a print workflow try engine.scene.setDesignUnit(.mm) // Verify the change let newUnit = try engine.scene.getDesignUnit() print("Design unit changed to:", newUnit) // .mm ``` ## Configuring DPI Access DPI through the scene's `scene/dpi` property. For print workflows, 300 DPI is the standard for high-quality output. ```swift highlight-designUnits-configureDpi // Set DPI to 300 for print-quality exports try engine.block.setFloat(scene, property: "scene/dpi", value: 300) // Read back the DPI value let dpi = try engine.block.getFloat(scene, property: "scene/dpi") print("DPI set to:", dpi) // 300.0 ``` DPI affects different aspects depending on the design unit: - **Physical units (mm, in)**: DPI determines the pixel resolution of exported files - **Pixel units**: DPI only affects the conversion of font sizes from points to pixels ## Setting Up Print-Ready Designs For print workflows, combine `setDesignUnit(.mm)` with appropriate DPI and page dimensions. Here's how to set up an A4 document ready for print export: ```swift highlight-designUnits-setPageDimensions // Set page to A4 dimensions (210 x 297 mm) try engine.block.setWidth(page, value: 210) try engine.block.setHeight(page, value: 297) let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) print("Page dimensions: \(pageWidth)mm x \(pageHeight)mm") ``` ## Font Sizes and Design Units Font sizes are always specified in points (`pt`), regardless of the scene's design unit. The DPI setting affects how points convert to pixels for rendering. ```swift highlight-designUnits-createTextBlock // Create a text block positioned and sized in millimeters let textBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: textBlock) // Position at 20 mm from left, 30 mm from top try engine.block.setPositionX(textBlock, value: 20) try engine.block.setPositionY(textBlock, value: 30) // Size: 170 mm wide, 50 mm tall try engine.block.setWidth(textBlock, value: 170) try engine.block.setHeight(textBlock, value: 50) try engine.block.setString( textBlock, property: "text/text", value: "This A4 document uses millimeter units with 300 DPI for print-ready output.", ) ``` When DPI changes, text blocks automatically adjust their rendered size to maintain visual consistency. ## Understanding Export Resolution The relationship between design units and export resolution is important for print workflows: ```swift highlight-designUnits-compareUnits // At 300 DPI: 1 inch = 300 pixels, 1 mm ≈ 11.81 pixels let a4WidthPixels = 210.0 * (300.0 / 25.4) let a4HeightPixels = 297.0 * (300.0 / 25.4) print("A4 at 300 DPI exports as \(Int(a4WidthPixels)) x \(Int(a4HeightPixels)) pixels") ``` At 300 DPI: - An A4 page (210 x 297 mm) exports as 2480 x 3508 pixels - A letter page (8.5 x 11 in) exports as 2550 x 3300 pixels ## Troubleshooting ### Exported Dimensions Don't Match Expected Size Verify that DPI is set correctly for physical units. At 300 DPI, 1 inch becomes 300 pixels. Check that your design unit matches your target output format. ### Text Appears Wrong Size After Unit Change Font sizes in points auto-adjust based on DPI. If text looks incorrect, verify the DPI setting matches your workflow requirements. ### Blocks Shift Position After Changing Units CE.SDK preserves visual appearance during unit conversion. If positions seem unexpected, check the original coordinate values — the numeric values change but visual positions should remain stable. ## API Reference | Method | Purpose | | --- | --- | | `engine.scene.getDesignUnit()` | Get the current design unit of the scene | | `engine.scene.setDesignUnit(_ designUnit:)` | Set the design unit for the scene | | `engine.block.getFloat(_:property: "scene/dpi")` | Get the DPI value of a scene | | `engine.block.setFloat(_:property: "scene/dpi", value:)` | Set the DPI value of a scene | | `engine.block.setWidth(_:value:)` | Set block width in current design unit | | `engine.block.setHeight(_:value:)` | Set block height in current design unit | ## Next Steps - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) — Learn about scene structure and management - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Understand block types and properties --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Editor State" description: "Control how users interact with content by switching between edit modes like transform, crop, and text." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/edit-modes-1f5b6c/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Editor State](https://img.ly/docs/cesdk/mac-catalyst/concepts/edit-modes-1f5b6c/) --- ```swift file=@cesdk_swift_examples/engine-guides-editor-state/EditorState.swift reference-only import Foundation import IMGLYEngine @MainActor func editorState(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Add an image block to demonstrate Crop mode let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(imageBlock, value: 350) try engine.block.setHeight(imageBlock, value: 250) try engine.block.setPositionX(imageBlock, value: 50) try engine.block.setPositionY(imageBlock, value: 175) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.appendChild(to: page, child: imageBlock) // Add a text block to demonstrate Text mode let textBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: textBlock) try engine.block.replaceText(textBlock, text: "Edit this text") try engine.block.setTextFontSize(textBlock, fontSize: 48) try engine.block.setWidthMode(textBlock, mode: .auto) try engine.block.setHeightMode(textBlock, mode: .auto) try engine.block.setPositionX(textBlock, value: 450) try engine.block.setPositionY(textBlock, value: 275) // Subscribe to state changes using AsyncStream let stateTask = Task { for await _ in engine.editor.onStateChanged { let currentMode = engine.editor.getEditMode() print("Edit mode changed to: \(currentMode)") } } // Get the current edit mode (default is Transform) let initialMode = engine.editor.getEditMode() print("Initial edit mode: \(initialMode)") // Select the image block and switch to Crop mode try engine.block.select(imageBlock) engine.editor.setEditMode(.crop) print("Switched to Crop mode") // Switch back to Transform mode engine.editor.setEditMode(.transform) print("Switched back to Transform mode") // Get the cursor type to display the appropriate cursor let cursorType = engine.editor.getCursorType() print("Cursor type: \(cursorType)") // Returns: .arrow, .move, .moveNotPermitted, .resize, .rotate, or .text // Get cursor rotation for directional cursors like resize handles let cursorRotation = engine.editor.getCursorRotation() print("Cursor rotation (radians): \(cursorRotation)") // Select the text block and switch to Text mode to get cursor position try engine.block.select(textBlock) engine.editor.setEditMode(.text) // Get text cursor position in screen space let textCursorX = engine.editor.getTextCursorPositionInScreenSpaceX() let textCursorY = engine.editor.getTextCursorPositionInScreenSpaceY() print("Text cursor position: (\(textCursorX), \(textCursorY))") // Check if a user interaction is currently in progress let isInteracting = try engine.editor.unstable_isInteractionHappening() print("Is interaction happening: \(isInteracting)") // Clean up the state subscription stateTask.cancel() // Switch back to Transform mode engine.editor.setEditMode(.transform) } ``` Control how users interact with content on the canvas by switching between edit modes, subscribing to state changes, and reading cursor information. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-editor-state) Edit modes define what type of content users can currently modify on the canvas. Each mode enables different interaction behaviors — Transform mode for moving and resizing, Crop mode for adjusting content within frames, Text mode for inline text editing, and so on. The engine maintains the current edit mode as part of its state and notifies subscribers when it changes. This guide covers: - The five built-in edit modes (Transform, Crop, Text, Trim, Playback) - Switching edit modes programmatically - Subscribing to state changes for UI synchronization - Reading cursor type and rotation - Tracking text cursor position for overlays - Detecting active user interactions ## Setup Create a scene with an image block and a text block to demonstrate the different edit modes. ```swift highlight-editorState-setup let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Add an image block to demonstrate Crop mode let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(imageBlock, value: 350) try engine.block.setHeight(imageBlock, value: 250) try engine.block.setPositionX(imageBlock, value: 50) try engine.block.setPositionY(imageBlock, value: 175) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.appendChild(to: page, child: imageBlock) // Add a text block to demonstrate Text mode let textBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: textBlock) try engine.block.replaceText(textBlock, text: "Edit this text") try engine.block.setTextFontSize(textBlock, fontSize: 48) try engine.block.setWidthMode(textBlock, mode: .auto) try engine.block.setHeightMode(textBlock, mode: .auto) try engine.block.setPositionX(textBlock, value: 450) try engine.block.setPositionY(textBlock, value: 275) ``` ## Edit Modes CE.SDK supports five built-in edit modes, each designed for a specific type of interaction with canvas content. | Mode | Purpose | |------|---------| | `.transform` | Move, resize, and rotate blocks (default) | | `.crop` | Adjust media content within block frames | | `.text` | Edit text content inline | | `.trim` | Adjust clip start and end points (video scenes) | | `.playback` | Play video or audio content (limited interactions) | ### Getting the Current Mode Query the current mode with `engine.editor.getEditMode()`. The initial mode is always `.transform`. ```swift highlight-editorState-getEditMode // Get the current edit mode (default is Transform) let initialMode = engine.editor.getEditMode() print("Initial edit mode: \(initialMode)") ``` ### Switching Edit Modes Use `engine.editor.setEditMode(_:)` to change the current editing mode. The mode determines what interactions are available on selected blocks. ```swift highlight-editorState-setEditMode // Select the image block and switch to Crop mode try engine.block.select(imageBlock) engine.editor.setEditMode(.crop) print("Switched to Crop mode") // Switch back to Transform mode engine.editor.setEditMode(.transform) print("Switched back to Transform mode") ``` > **Tip:** Some modes only take effect when a compatible block is selected. For example, Crop mode has no visible effect unless an image or video block is selected, and Text mode requires a text block. ## Subscribing to State Changes The engine notifies subscribers whenever the editor state changes, including mode switches and cursor updates. Use the `onStateChanged` `AsyncStream` to react to changes. ```swift highlight-editorState-onStateChanged // Subscribe to state changes using AsyncStream let stateTask = Task { for await _ in engine.editor.onStateChanged { let currentMode = engine.editor.getEditMode() print("Edit mode changed to: \(currentMode)") } } ``` Common use cases include updating toolbar UI to reflect the current mode, showing mode-specific panels, and disabling actions during Playback mode. Cancel the task when you no longer need updates to prevent unnecessary work. ## Cursor State The engine tracks what cursor type should be displayed based on the current context and hovered element. These APIs are most relevant for iPad apps with mouse or trackpad input (iPadOS 13.4+) and macOS apps, where users interact with a pointer. On iPhone, touch interactions don't use a visible cursor, so you can skip this section if you're targeting iPhone only. ### Reading Cursor Type Use `engine.editor.getCursorType()` to get the cursor type to display. ```swift highlight-editorState-cursorType // Get the cursor type to display the appropriate cursor let cursorType = engine.editor.getCursorType() print("Cursor type: \(cursorType)") // Returns: .arrow, .move, .moveNotPermitted, .resize, .rotate, or .text ``` | Cursor Type | Meaning | |-------------|---------| | `.arrow` | Default pointer cursor | | `.move` | Element can be moved | | `.moveNotPermitted` | Element cannot be moved in the current context | | `.resize` | Resize handle is hovered | | `.rotate` | Rotation handle is hovered | | `.text` | Text editing cursor | ### Reading Cursor Rotation For directional cursors like resize handles, use `engine.editor.getCursorRotation()` to get the rotation angle in radians. Apply this rotation to your cursor image for correct visual feedback. ```swift highlight-editorState-cursorRotation // Get cursor rotation for directional cursors like resize handles let cursorRotation = engine.editor.getCursorRotation() print("Cursor rotation (radians): \(cursorRotation)") ``` ## Text Cursor Position When in Text edit mode, track the text cursor (caret) position for rendering custom overlays or toolbars near the insertion point. Use `getTextCursorPositionInScreenSpaceX()` and `getTextCursorPositionInScreenSpaceY()` to get the cursor position in screen pixels. These values update as the user moves through text. ```swift highlight-editorState-textCursorPosition // Select the text block and switch to Text mode to get cursor position try engine.block.select(textBlock) engine.editor.setEditMode(.text) // Get text cursor position in screen space let textCursorX = engine.editor.getTextCursorPositionInScreenSpaceX() let textCursorY = engine.editor.getTextCursorPositionInScreenSpaceY() print("Text cursor position: (\(textCursorX), \(textCursorY))") ``` ## Detecting Active Interactions Call `engine.editor.unstable_isInteractionHappening()` to check if a user interaction like dragging or resizing is in progress. This is useful for deferring expensive operations until after the interaction completes. ```swift highlight-editorState-interactionHappening // Check if a user interaction is currently in progress let isInteracting = try engine.editor.unstable_isInteractionHappening() print("Is interaction happening: \(isInteracting)") ``` > **Warning:** This API is marked unstable and may change in future releases. ## Next Steps - [Undo and History](https://img.ly/docs/cesdk/mac-catalyst/concepts/undo-and-history-99479d/) — Implement undo/redo functionality and manage history stacks - [Events](https://img.ly/docs/cesdk/mac-catalyst/concepts/events-353f97/) — Subscribe to block creation, update, and deletion events - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Understand block types and the design hierarchy - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) — Learn about scene structure and page management --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Editing Workflow" description: "Control editing access with Creator and Adopter roles, each offering tailored permissions and UI constraints." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) --- CE.SDK controls editing access through roles and scopes, enabling template workflows where designers create locked layouts and end-users customize only permitted elements. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-editing-workflow) CE.SDK uses a two-tier permission system: **roles** define user types with preset permissions, while **scopes** control specific capabilities. This enables workflows where templates can be prepared by designers and safely customized by end-users. ```swift file=@cesdk_swift_examples/engine-guides-editing-workflow/EditingWorkflow.swift reference-only import Foundation import IMGLYEngine @MainActor func editingWorkflow(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 100) try engine.block.setHeight(block, value: 100) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.appendChild(to: page, child: block) // Roles define user types: "Creator", "Adopter", "Viewer", "Presenter" let role = try engine.editor.getRole() print("Current role:", role) // "Creator" // Switch to a different role try engine.editor.setRole("Adopter") print("New role:", try engine.editor.getRole()) // "Adopter" // Switch back to Creator for the rest of the guide try engine.editor.setRole("Creator") // Set global scopes to 'Defer' so block-level settings take effect try engine.editor.setGlobalScope(key: "editor/select", value: .defer) try engine.editor.setGlobalScope(key: "layer/move", value: .defer) try engine.editor.setGlobalScope(key: "text/edit", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/destroy", value: .defer) // Query a global scope value let moveScope = try engine.editor.getGlobalScope(key: "layer/move") print("Global 'layer/move' scope:", moveScope) // .defer // List all available scopes let allScopes = try engine.editor.findAllScopes() print("Available scopes:", allScopes.count) // Lock the block — Adopters cannot select, move, or delete it try engine.block.setScopeEnabled(block, key: "editor/select", enabled: false) try engine.block.setScopeEnabled(block, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(block, key: "lifecycle/destroy", enabled: false) // Query a block-level scope let canMove = try engine.block.isScopeEnabled(block, key: "layer/move") print("Block 'layer/move' enabled:", canMove) // false // Check the final resolved permission (role + global + block scopes) let isAllowed = try engine.block.isAllowedByScope(block, key: "layer/move") print("Moving allowed:", isAllowed) // false (global is .defer, block is disabled) // Switch to Adopter — restrictions now apply try engine.editor.setRole("Adopter") let isAllowedAsAdopter = try engine.block.isAllowedByScope(block, key: "layer/move") print("Moving allowed as Adopter:", isAllowedAsAdopter) // false // Switch back to Creator — full access restored try engine.editor.setRole("Creator") let isAllowedAsCreator = try engine.block.isAllowedByScope(block, key: "layer/move") print("Moving allowed as Creator:", isAllowedAsCreator) // true } ``` This guide covers: - The four user roles and their purposes - How scopes control editing capabilities - The permission resolution hierarchy - Common template workflow patterns ## Roles Roles define user types with different default permissions: | Role | Purpose | Default Access | |------|---------|----------------| | **Creator** | Designers building templates | Full access to all operations | | **Adopter** | End-users customizing templates | Limited by block-level scopes | | **Viewer** | Static preview without interaction | Read-only, no playback controls | | **Presenter** | Presenting slideshows or playing videos | Read-only with playback and navigation | Creators set the block-level scopes that constrain what Adopters can do. This separation enables brand consistency while allowing personalization. ```swift highlight-editingWorkflow-roles // Roles define user types: "Creator", "Adopter", "Viewer", "Presenter" let role = try engine.editor.getRole() print("Current role:", role) // "Creator" // Switch to a different role try engine.editor.setRole("Adopter") print("New role:", try engine.editor.getRole()) // "Adopter" // Switch back to Creator for the rest of the guide try engine.editor.setRole("Creator") ``` ## Scopes Scopes define specific capabilities organized into categories: - **Text**: Editing content and character formatting - **Fill/Stroke**: Changing colors and shapes - **Layer**: Moving, resizing, rotating, cropping - **Appearance**: Filters, effects, shadows, animations - **Lifecycle**: Deleting and duplicating elements - **Editor**: Adding new elements and selecting ## Global vs Block-Level Scopes **Global scopes** apply editor-wide and determine whether block-level settings are checked: - `.allow` — Always permit the operation - `.deny` — Always block the operation - `.defer` — Check block-level scope settings **Block-level scopes** control permissions on individual blocks. These settings only take effect when the corresponding global scope is set to `.defer`. ```swift highlight-editingWorkflow-globalScopes // Set global scopes to 'Defer' so block-level settings take effect try engine.editor.setGlobalScope(key: "editor/select", value: .defer) try engine.editor.setGlobalScope(key: "layer/move", value: .defer) try engine.editor.setGlobalScope(key: "text/edit", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/destroy", value: .defer) // Query a global scope value let moveScope = try engine.editor.getGlobalScope(key: "layer/move") print("Global 'layer/move' scope:", moveScope) // .defer // List all available scopes let allScopes = try engine.editor.findAllScopes() print("Available scopes:", allScopes.count) ``` To lock a specific block, disable its scopes: ```swift highlight-editingWorkflow-blockScopes // Lock the block — Adopters cannot select, move, or delete it try engine.block.setScopeEnabled(block, key: "editor/select", enabled: false) try engine.block.setScopeEnabled(block, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(block, key: "lifecycle/destroy", enabled: false) // Query a block-level scope let canMove = try engine.block.isScopeEnabled(block, key: "layer/move") print("Block 'layer/move' enabled:", canMove) // false ``` ## Permission Resolution Permissions resolve in this order: 1. **Role defaults** — Each role has preset global scope values 2. **Global scope** — If `.allow` or `.deny`, this is the final answer 3. **Block-level scope** — If global is `.defer`, check the block's settings Use `isAllowedByScope(_:key:)` to check the final computed permission for any block and scope combination: ```swift highlight-editingWorkflow-checkPermissions // Check the final resolved permission (role + global + block scopes) let isAllowed = try engine.block.isAllowedByScope(block, key: "layer/move") print("Moving allowed:", isAllowed) // false (global is .defer, block is disabled) ``` ## Switching Roles Change roles at runtime with `setRole(_:)`. When switching to Adopter, block-level restrictions take effect. Switching back to Creator restores full access. ```swift highlight-editingWorkflow-switchRole // Switch to Adopter — restrictions now apply try engine.editor.setRole("Adopter") let isAllowedAsAdopter = try engine.block.isAllowedByScope(block, key: "layer/move") print("Moving allowed as Adopter:", isAllowedAsAdopter) // false // Switch back to Creator — full access restored try engine.editor.setRole("Creator") let isAllowedAsCreator = try engine.block.isAllowedByScope(block, key: "layer/move") print("Moving allowed as Creator:", isAllowedAsCreator) // true ``` ## Customizing Role Behavior The `onRoleChanged` property provides an `AsyncStream` that fires after role defaults are applied. Use it to customize scopes per role: ```swift // Subscribe to role changes Task { for await role in engine.editor.onRoleChanged { if role == "Adopter" { // Enable filters for adopters even though normally restricted try engine.editor.setGlobalScope(key: "appearance/filter", value: .allow) } } } ``` > **Warning:** The `onRoleChanged` stream fires *after* role defaults are applied. Any scope changes you make in the callback override the defaults. ## Template Workflow Pattern A typical template workflow: 1. **Designer (Creator)** creates the template layout 2. **Designer** locks brand elements using block scopes 3. **Designer** keeps personalization fields editable 4. **End-user (Adopter)** opens the template 5. **End-user** edits only permitted elements 6. **End-user** exports the personalized result This pattern ensures brand consistency while enabling personalization. ## Next Steps - [Lock Design Elements](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) — Step-by-step instructions for locking specific elements in templates. - [Set Editing Constraints](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) — Learn how to control editing capabilities in CE.SDK templates using the Scope system to lock positions, prevent transformations, and create guided editing experiences. - [Editor State](https://img.ly/docs/cesdk/mac-catalyst/concepts/edit-modes-1f5b6c/) — Track edit modes and selection to react to workflow transitions. - [Events](https://img.ly/docs/cesdk/mac-catalyst/concepts/events-353f97/) — Subscribe to block creation, update, and deletion. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Error Catalog" description: "Reference of every structured CE.SDK engine error code, its message, hint, and related documentation page." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/error-catalog-z3djzn/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Error Catalog](https://img.ly/docs/cesdk/mac-catalyst/concepts/error-catalog-z3djzn/) --- Every recoverable engine failure is a structured error with a stable `code` (for example `SCENE.NOT_VALID`), an English developer-facing message and hint, typed arguments, and an optional documentation link. Match on the `code` rather than the message string — the code is stable across releases. For how to read these on each binding, see the [structured-errors migration guide](#broken-link-e7f3a1). This page lists all catalog errors grouped by category including links to documentation pages. The `code` is what you branch on. The `message` and `hint` are the English strings the engine renders (developer-facing — surface localized copy in your UI layer). ## ASSET Asset sources, asset library, asset references. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `ASSET.CANNOT_APPLY_COLOR_NO_TARGET` | Could not apply color asset to block \{block}. Block has nothing to apply color to. | The block has no fill, stroke, or text color to receive the asset. Set the relevant property type first. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.COLOR_CMYK_MISSING_FIELDS` | \`payload.color\` with \`CMYK\` color space must have fields \`c\`, \`m\`, \`y\`, \`k\`. | CMYK colors require numeric \`c\`, \`m\`, \`y\`, \`k\` components. Add them to the payload. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.COLOR_MISSING` | Asset does not contain a color. | Color asset operations require a \`color\` payload. Verify the asset is a color asset. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.COLOR_MISSING_COLOR_SPACE` | \`payload.color\` must have a \`colorSpace\` field. | Add \`colorSpace\` (sRGB, CMYK, or SpotColor) to the asset payload. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.COLOR_SPACE_UNKNOWN` | Unknown color space: \{colorSpace} | Color space '\{colorSpace}' is not supported. Valid values are sRGB, CMYK, and SpotColor. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.COLOR_SPOT_MISSING_FIELDS` | \`payload.color\` with \`SpotColor\` color space must have fields \`name\`, \`externalReference\`, \`representation\`. | Spot colors require all three fields; \`representation\` must itself be a sub-color in sRGB or CMYK. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.COLOR_SPOT_REPRESENTATION_INVALID` | A \`payload.color\` with colorSpace \`SpotColor\` must use a \`RGB\` or \`CMYK\` color for its \`representation\` field. | Spot color representation must be a primitive sRGB or CMYK value. Nested SpotColor is not allowed. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.COLOR_SRGB_MISSING_FIELDS` | \`payload.color\` with \`sRGB\` color space must have fields \`r\`, \`g\`, and \`b\`. | sRGB colors require numeric \`r\`, \`g\`, \`b\` components. Add them to the payload. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.DOCUMENT_SOURCE_NO_ADD` | A document asset source does not allow adding assets. | Document asset sources are read-only views into the active scene. Add assets to a local asset source instead. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.DOCUMENT_SOURCE_NO_REMOVE` | A document asset source does not allow removing assets. | Document asset sources are read-only. Remove the underlying scene blocks instead. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FACET_PATH_NOT_FACETABLE` | Asset property '\{property}' is not facetable. Use 'tags', 'groups', or 'meta.\'. | Facets enumerate bounded value sets; 'label' and 'id' are unbounded. Request 'tags', 'groups', or 'meta.\' instead. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_COMBINATOR_EMPTY` | Asset filter '\{combinator}' must have at least one child. | Add at least one filter expression to the '\{combinator}' array. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_COMBINATOR_NOT_ARRAY` | Asset filter '\{combinator}' must be an array. | Provide an array of filter expressions for '\{combinator}'. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_META_KEY_MISSING` | Asset property filter 'meta' path must include a key (e.g. 'meta.languages'). | Append the meta key after 'meta.' — for example 'meta.languages'. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_MULTIPLE_DISCRIMINATORS` | Asset filter has multiple discriminators; specify exactly one of 'property', 'and', 'or', 'not'. | Keep only one of 'property', 'and', 'or', 'not' on the expression. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_NOT_CHILD_NOT_OBJECT` | Asset filter '\{combinator}' must be an object. | Provide a single filter-expression object as the value of '\{combinator}'. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_NOT_OBJECT` | Asset filter must be an object. | Wrap each filter expression in an object with one of 'property', 'and', 'or', 'not'. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_NO_DISCRIMINATOR` | Asset filter must have exactly one of 'property', 'and', 'or', 'not'. | Set one discriminator key on the filter expression. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_OPERAND_MISSING` | Asset property filter must have exactly one of 'contains' or 'equals'. | Set either 'contains' or 'equals' on the property filter, not both and not neither. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_OPERAND_NOT_STRING` | Asset property filter '\{operand}' must be a string. | Provide a string value for '\{operand}' on the property filter. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_PROPERTY_EMPTY` | Asset property filter 'property' must not be empty. | Set 'property' to 'label', 'id', 'tags', 'groups', or 'meta.\'. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_PROPERTY_NOT_STRING` | Asset property filter must have a string 'property'. | Provide 'property' as a string identifying the asset field to match. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_PROPERTY_UNKNOWN` | Unknown asset property '\{property}'. Use 'label', 'id', 'tags', 'groups', or 'meta.\'. | Replace '\{property}' with one of the supported property roots. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_ROOT_NOT_ARRAY` | Asset filter must be an array of filter expressions. | Pass the top-level filter as a JSON array of filter expressions (or null/empty for no filter). | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FILTER_UNRECOGNIZED_DISCRIMINATOR` | Asset filter has no recognized discriminator. | Use one of 'property', 'and', 'or', 'not' on the filter expression. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FIND_FN_REQUIRED` | findAssetsFn is required. | Pass a non-null findAssetsFn callback when registering a custom asset source. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FONT_MISSING_NAME` | Missing required field 'name' in font. | Each font entry needs a \`name\`. Add it to the payload. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FONT_MISSING_URL` | Missing required field 'url' in font. | Each font entry needs a \`url\` pointing at the font file. Add it to the payload. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.FONT_WEIGHT_UNKNOWN` | Unknown font weight '\{weight}'. | Use thin, extraLight, light, normal, medium, semiBold, bold, extraBold or heavy. The OpenType names Ultra Light, Regular, Demi Bold, Ultra Bold and Black work too, in any letter case. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.ID_ALREADY_EXISTS` | Asset with id \{id} already exists in asset source. | Each asset id must be unique within a source. Remove the existing asset, or use a different id. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.JSON_INVALID_IN_URI` | Invalid JSON content in URI: \{uri} | The JSON at '\{uri}' could not be parsed. Validate the payload or check the URL response body. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.JSON_MALFORMED_LOCAL` | Invalid JSON content for local asset source. Expected 'id' and 'assets' fields. | The JSON payload must include top-level 'id' and 'assets' fields. See the local-asset-source schema for the full shape. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.META_NON_STRING_ENTRY` | Unexpectedly found non-string entry in asset's meta. | Asset meta is a flat string map. Numeric or object values are not supported — stringify them before inserting. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.NO_SELECTION` | No elements selected. | This asset application requires a selected block. Verify a selection exists with api.block.findAllSelected() before applying. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.RESOURCE_DATA_NOT_AVAILABLE` | Resource data not available: \{uri} | The resource at '\{uri}' is registered but has no data buffer. Confirm the source provides bytes (not just metadata). | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.RESOURCE_DATA_UNAVAILABLE` | Resource data is not available. | The resource has no attached data provider. Ensure the resource has been fetched and is ready before requesting its data. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.RESOURCE_NOT_FOUND_AT_URI` | Resource not found: \{uri} | No resource registered at '\{uri}'. Confirm the asset source serves the URI and that loading completed. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.RESOURCE_NOT_READY` | Resource is not ready. | The resource is still loading or in an error state. Wait for the Ready state, or handle the error via the resource's loading state. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.RESOURCE_NOT_READY_AT_URI` | Resource not ready: \{uri} | The resource at '\{uri}' is still loading. Subscribe to resource events or retry after the resource is Ready. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SORT_KEY_MISSING` | Sort key not found in asset source. | The asset source does not provide the requested sort key. Use a sort key the source declares, or omit the sort parameter. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SOURCE_ADD_DENIED` | Assets cannot be added to this asset source: \{sourceId}. | The source '\{sourceId}' rejected addAssets(). It may be backed by an immutable provider. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SOURCE_ALREADY_EXISTS` | AssetSource with id \{sourceId} already exists. | Asset source ids must be unique. Remove the existing source via removeAssetSource() or pick a different id. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SOURCE_CALLBACK_MISSING` | Asset source \{sourceId} was registered without a \{callback} callback. | The source '\{sourceId}' did not provide \{callback}() when it was registered, so it cannot perform this operation. Register the source with that callback, or do not call this operation on it. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SOURCE_CANNOT_ADD_ASSETS` | AssetSource with id \{sourceId} cannot add assets. | The source '\{sourceId}' is read-only. Modify the underlying data store or pick a writable source. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SOURCE_CANNOT_REMOVE_ASSETS` | AssetSource with id \{sourceId} cannot remove assets. | The source '\{sourceId}' is read-only. Modify the underlying data store or pick a writable source. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SOURCE_DOES_NOT_SUPPORT_APPLY_PROPERTIES` | Asset source \{source} does not support applying properties. | The source '\{source}' does not implement applyProperties(). Use a source that supports it, or apply properties through a different surface. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SOURCE_NOT_EXISTS` | AssetSource with id \{sourceId} does not exist. | No asset source registered under '\{sourceId}'. Use api.asset.addLocalAssetSource() or addAssetSource() to register one first. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SOURCE_NOT_REMOVABLE` | AssetSource with id \{sourceId} may not be removed. | Built-in asset sources cannot be unregistered. Override their behavior by adding a higher-priority custom source instead. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SOURCE_NO_PROPERTY_MOD` | This asset source does not support property modification. | The active asset source does not support property modification. Register a writable source via api.asset.addLocalAssetSource(), or modify the underlying assets directly. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SOURCE_REMOVE_DENIED` | Assets cannot be removed from this asset source: \{sourceId}. | The source '\{sourceId}' rejected removeAssets(). It may be backed by an immutable provider. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.SOURCE_UNKNOWN` | The asset source \{sourceId} is unknown. | No asset source registered under '\{sourceId}'. Add it before requesting its content. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.STYLE_PRESET_INVALID_PAYLOAD` | Style preset payload is not a valid JSON object. | The asset's payload.stylePreset must be a JSON object. Check the asset definition's stylePreset field. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.STYLE_PRESET_MISSING_BLOCK_TYPE` | A style preset requires a blockType to create a new block. | When applying a style preset without a target block, set meta.blockType so the engine knows which block type to create. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.STYLE_PRESET_NOT_APPLICABLE` | Style preset is not applicable to a block of type '\{blockType}'. | The style preset declares the block types it supports. Apply it to a supported block type, or widen the preset's blockType. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.TARGET_BLOCK_NOT_VALID` | Block is not valid. | The selected block is no longer valid. Re-resolve it via api.block.findAllSelected() and confirm with api.block.isValid(block) before applying an asset. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.TRANSFORM_PRESET_FIXED_ASPECT_MISSING_FIELDS` | \`payload.transformPreset\` of type \`fixedAspectRatio\` must have fields \`width\`, and \`height\`. | FixedAspectRatio presets need \`width\` and \`height\` to determine the ratio. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.TRANSFORM_PRESET_FIXED_SIZE_MISSING_FIELDS` | \`payload.transformPreset\` of type \`fixedAspectRatio\` must have fields \`width\`, \`height\`, and \`designUnit\`. | FixedSize presets need numeric \`width\`, \`height\`, and a \`designUnit\` (e.g. 'Pixel', 'Inch'). | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.TRANSFORM_PRESET_MISSING_TYPE` | \`payload.transformPreset\` must have a \`type\` field. | Add a \`type\` to the transformPreset payload (e.g. 'FixedSize', 'FixedAspectRatio', 'ContentAspectRatio'). | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.TRANSFORM_PRESET_TYPE_UNKNOWN` | Unknown transformPreset type: \{type} | TransformPreset type '\{type}' is not recognized. Valid values: 'FixedSize', 'FixedAspectRatio', 'ContentAspectRatio'. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.TYPEFACE_MISSING_FAMILY` | Missing required field 'family' in typeface. | Typefaces declare a \`family\` grouping their fonts. Add it to the payload. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.TYPEFACE_MISSING_WEIGHT` | Missing required field 'weight' in typeface. | Typefaces declare a \`weight\`. Add it to the payload. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.UNSUPPORTED_MIME_TYPE` | Unsupported mime type for loadWithoutService: \{mimeType} | The bypass loader cannot handle '\{mimeType}'. Route through the regular resource service or extend the loader. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.UNSUPPORTED_MIME_TYPE_FOR_BLOCK` | Cannot create a block from an asset with the MIME type \{mimeType}. | MIME type '\{mimeType}' has no block factory. Convert the asset to a supported type (image/*, video/*, audio/\*) or register a custom factory. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.URI_INVALID_BARE` | Invalid URI \{uri}. | The asset URI is malformed. Use a valid absolute URL or a registered scheme like 'buffer://'. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | | `ASSET.URI_META_MISSING` | 'uri' not found in asset 'meta'. Can't replace. | The asset's meta block must include a 'uri' field for replace() to work. Add it before invoking. | [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) | ## ASYNC Asynchronous operations and their handles, such as a cancel through cancelAsyncOperation. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `ASYNC.OPERATION_CANCELLED` | The operation was cancelled. | The operation was cancelled through its handle, for example because its task, coroutine or abort signal was cancelled. It delivers no further result. Start it again if the result is still needed. | | ## AUDIO Audio playback and routing. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `AUDIO.DATA_SOURCE_INIT_FAILED` | Mini Audio data source initialization failed. | miniaudio could not allocate the data source. Out-of-memory or invalid decoder state is the likely cause. | | | `AUDIO.DATA_SOURCE_NODE_INIT_FAILED` | Data source node initialization failed (\{resultCode}). | miniaudio could not attach the data source as a node with error \{resultCode}. The graph configuration may be invalid. | | | `AUDIO.DATA_SOURCE_NO_DURATION` | Audio data source has no known duration. | miniaudio could not determine the source length. Streaming or seekless sources may not report a duration; decode the source fully or supply one with a known length. | | | `AUDIO.DECODER_FORMAT_FAILED` | Failed to get decoder data format. | The decoder reported an indeterminate sample format. The file header may be truncated or malformed. | | | `AUDIO.DECODER_INIT_FAILED` | Mini Audio decoder initialization failed (\{resultCode}). | miniaudio rejected the audio source with error \{resultCode}. The file may be corrupted or use an unsupported codec. | | | `AUDIO.DEVICE_INIT_FAILED` | Initializing audio device failed. | The platform's audio device could not be opened. Verify the device exists and is not in use by another process. | | | `AUDIO.DEVICE_RESUME_FAILED` | Failed to resume the audio device. | The audio device was suspended and could not be resumed. On web, trigger playback from within a user-gesture handler (e.g. a click) so the browser unlocks the audio context before calling play. | | | `AUDIO.DEVICE_START_FAILED` | Audio device start failed. | The audio device opened but did not start playback. Check device state and platform permissions. | | | `AUDIO.DEVICE_STOP_FAILED` | Audio device stop failed. | The audio device could not be stopped cleanly. Subsequent restart may be required. | | | `AUDIO.INVALID_SOUND_HANDLE` | Invalid sound handle. | The sound handle does not refer to a live audio source. It may have been stopped or never created. | | | `AUDIO.NODE_ATTACH_OUTPUT_BUS_FAILED` | Attaching output bus failed (\{resultCode}). | miniaudio could not attach the node's output bus with error \{resultCode}. The graph layout may be invalid. | | | `AUDIO.NODE_GRAPH_INIT_FAILED` | Audio node graph init failed. | The miniaudio node graph could not be created. Check available system resources and the audio backend installation. | | | `AUDIO.NODE_SET_STATE_FAILED` | Setting node state failed (\{resultCode}). | miniaudio rejected the node state transition with error \{resultCode}. | | | `AUDIO.NODE_STATE_CHANGE_FAILED` | Failed to change sound node state to \{targetState}. | miniaudio did not converge on the requested node state '\{targetState}'. The node may have been destroyed concurrently. | | | `AUDIO.PCM_READ_FAILED` | Failed to read PCM frames from node graph (\{resultCode}). | miniaudio returned error \{resultCode} while reading PCM. The graph may be in an invalid state; re-create it. | | | `AUDIO.RESAMPLER_INIT_FAILED` | Mini Audio resampler initialization failed. | miniaudio could not construct a resampler for the source rate. The source sample rate may be unsupported. | | | `AUDIO.UNSUPPORTED_CODEC` | Unsupported audio codec. | miniaudio cannot decode this audio format. Re-encode the source as WAV, FLAC, or one of the documented supported codecs. | [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/import-media/file-format-support-8cdc84/) | ## BINDING Errors raised at the binding bridge layer (WASM/JNI/ObjC++/N-API parameter validation, host-callback invariants, asset-source platform plumbing). | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `BINDING.ASSET_PLATFORM_SOURCE_UNAVAILABLE` | Platform source object is unavailable. | The platform-side asset source backing this engine handle was garbage collected. Keep a reference while the engine may call into it. | | | `BINDING.ASSET_SOURCE_ADD_UNSUPPORTED` | Assets cannot be added to this asset source. | Implement \`addAsset\` on the asset source if you need this operation to succeed. | | | `BINDING.ASSET_SOURCE_APPLY_PROPERTIES_UNSUPPORTED` | Asset properties cannot be applied by this asset source. | Implement \`applyAssetProperties\` on the asset source if you need this operation to succeed. | | | `BINDING.ASSET_SOURCE_FETCH_UNSUPPORTED` | Assets cannot be fetched from this asset source. | Implement \`fetchAsset\` on the asset source if you need this operation to succeed. | | | `BINDING.ASSET_SOURCE_REMOVE_UNSUPPORTED` | Assets cannot be removed from this asset source. | Implement \`removeAsset\` on the asset source if you need this operation to succeed. | | | `BINDING.FACETS_QUERY_ENTRY_NOT_STRING` | Each entry in 'facets' must be a property path string | Every \`facets\` entry must be a string property path such as \`'groups'\`, \`'tags'\`, or \`'meta.\'\`. | | | `BINDING.FACETS_QUERY_NOT_ARRAY` | Field 'facets' must be an array of property path strings | Pass \`facets\` as a JavaScript array of property path strings such as \`'groups'\`, \`'tags'\`, or \`'meta.\'\`. | | | `BINDING.FACETS_RESULT_NOT_OBJECT` | Field 'facets' in find assets result is not an object keyed by facet path | Return \`facets\` from a custom source's \`findAssets\` as an object keyed by the requested property paths. | | | `BINDING.FACET_COUNT_NOT_NUMBER` | Field 'count' in facet '\{facet}' of find assets result is not a number | Omit \`count\` or set it to a number; it is optional per bucket. | | | `BINDING.FACET_ENTRY_NOT_OBJECT` | An entry of facet '\{facet}' in find assets result is not a value/count object | Each facet bucket must be an object with a string \`value\` and an optional numeric \`count\`. | | | `BINDING.FACET_NOT_ARRAY` | Facet '\{facet}' in find assets result is not an array of value/count entries | Each \`facets\[path]\` must be an array of value/count buckets. | | | `BINDING.FACET_VALUE_MISSING` | Missing required string field 'value' in facet '\{facet}' of find assets result | Every facet bucket must carry a string \`value\`. | | | `BINDING.HOST_CALLBACK_THREW` | \{operation} callback threw: \{message} | The platform-side \`\{operation}\` callback raised an error. Inspect \`args.message\` for the original host-language exception text and ensure the callback handles its input correctly. | | | `BINDING.JSON_NOT_REPRESENTABLE` | Value cannot be represented as JSON. | Pass only JSON-serializable values (no functions, no cycles, no \`BigInt\`). | | | `BINDING.JSON_PARSE_FAILED` | Failed to parse JSON from JS value. | Pass a value the bridge can stringify with \`JSON.stringify\` without throwing. | | | `BINDING.NODE_NOT_INITIALIZED` | Not initialized | Call \`CreativeEngine.init(...)\` before invoking other engine methods. | | | `BINDING.NO_TEXT_BLOCK_BEING_EDITED` | No text block is currently being edited. | Enter text editing mode via \`block.setTextEditMode(...)\` before invoking text-editing APIs. | | | `BINDING.URI_RESOLVER_INVALID_RESULT` | URI resolver returned an invalid result. | Return a non-null string URL (or throw) from the URI resolver callback. | | | `BINDING.URI_RESOLVER_PROMISE_REJECTED` | URI resolver promise rejected. | Resolve the returned promise with a string URL, or reject with a descriptive error message. | | | `BINDING.URI_RESOLVER_UNAVAILABLE` | Async URI resolver is unavailable. | Configure an async URI resolver on the editor before triggering this call. | | | `BINDING.WASM_ASSET_FILTER_JSON_PARSE_FAILED` | Failed to parse asset filter JSON from JS. | Pass an object the JS engine can serialize to JSON (no functions, no cycles, no \`BigInt\`). | | | `BINDING.WASM_ASSET_GROUPS_NOT_ARRAY` | Field 'groups' is not an array | Pass \`groups\` as a JavaScript array of group id strings. | | | `BINDING.WASM_ASSET_MISSING_ID` | Missing required field 'id' in asset | Each asset definition must carry an \`id\` string. | | | `BINDING.WASM_ASSET_PROPERTY_MISSING_DEFAULT_VALUE` | Missing required field 'defaultValue' in asset property | Set the property's \`defaultValue\` field to a value matching its declared \`type\` (same shape as \`value\`). | | | `BINDING.WASM_ASSET_PROPERTY_MISSING_MAX` | Missing required field 'max' in asset property | Provide a numeric \`max\` on a numeric asset property. | | | `BINDING.WASM_ASSET_PROPERTY_MISSING_MIN` | Missing required field 'min' in asset property | Provide a numeric \`min\` on a numeric asset property. | | | `BINDING.WASM_ASSET_PROPERTY_MISSING_OPTIONS` | Missing required field 'options' in asset property | Provide an \`options\` array on an \`enum\`-type asset property. | | | `BINDING.WASM_ASSET_PROPERTY_MISSING_STEP` | Missing required field 'step' in asset property | Provide a numeric \`step\` on a numeric asset property. | | | `BINDING.WASM_ASSET_PROPERTY_MISSING_TYPE` | Missing required field 'type' in asset property | Set the property's \`type\` to a supported value: \`String\`, \`Boolean\`, \`Color\`, \`Enum\`, \`Int\`, \`Float\`, or \`Double\`. | | | `BINDING.WASM_ASSET_PROPERTY_MISSING_VALUE` | Missing required field 'value' in asset property | Set the property's \`value\` field to a value matching its declared \`type\` (e.g. a number for \`Int\`/\`Float\`/\`Double\`, a string for \`String\`/\`Enum\`). | | | `BINDING.WASM_ASSET_PROPERTY_UNKNOWN_TYPE` | Unknown property type: \{type} | '\{type}' is not a valid asset property type. Use one of \`String\`, \`Boolean\`, \`Color\`, \`Enum\`, \`Int\`, \`Float\`, or \`Double\`. | | | `BINDING.WASM_ASSET_RESULT_MISSING_ID` | Missing required field 'id' in asset result | Each asset result must carry an \`id\` string. | | | `BINDING.WASM_ASSET_RESULT_MISSING_SOURCE_ID` | Missing required field 'context.sourceId' in asset result | Set \`context.sourceId\` on each asset result to the asset source id that produced it. | | | `BINDING.WASM_BLOCK_STATE_MISSING_ERROR` | Missing required field 'error' in block state | Pass an \`error\` field on a block state with \`type: 'Error'\`. | | | `BINDING.WASM_BLOCK_STATE_MISSING_PROGRESS` | Missing required field 'progress' in block state | Pass a numeric \`progress\` field on a block state with \`type: 'Pending'\`. | | | `BINDING.WASM_BLOCK_STATE_MISSING_TYPE` | Missing required field 'type' in block state | Pass a \`type\` field of \`'Ready'\`, \`'Pending'\`, or \`'Error'\` on the block state object. | | | `BINDING.WASM_BLOCK_STATE_UNHANDLED` | Unhandled state error. | Internal bridge error — file a bug if you encounter this with a reproducible block state. | | | `BINDING.WASM_BLOCK_STATE_UNKNOWN_ERROR` | Unknown block state error: \{error} | Use one of the documented \`BlockStateError\` enum values. | | | `BINDING.WASM_BLOCK_STATE_UNKNOWN_TYPE` | Unknown block state type: \{type} | Use one of \`'Ready'\`, \`'Pending'\`, \`'Error'\`. | | | `BINDING.WASM_COLOR_MISSING_COLOR_SPACE` | Missing colorSpace field in the color object. | Provide a \`colorSpace\` value of \`'sRGB'\`, \`'CMYK'\`, or \`'SpotColor'\` when constructing a color from JavaScript. | | | `BINDING.WASM_COLOR_MISSING_COMPONENTS` | Missing components field in the color object. | Provide a \`components\` array of channel values matching the color space (3 for sRGB, 4 for CMYK). | | | `BINDING.WASM_COLOR_MISSING_EXTERNAL_REFERENCE` | Missing externalReference field in the color object. | Set \`externalReference\` (empty string is allowed) when constructing a color from JavaScript. | | | `BINDING.WASM_COLOR_MISSING_SPOT_NAME` | Missing spotColorName field in the color object. | Set \`spotColorName\` to a non-empty string when constructing a SpotColor. | | | `BINDING.WASM_COLOR_MISSING_TINT` | Missing tint field in the color object. | Set \`tint\` to a number between 0 and 1 when constructing a color from JavaScript. | | | `BINDING.WASM_COLOR_PARSE_FAILED` | Could not parse color. | Pass a color object with \`colorSpace\` and the matching channel fields, not a raw value. | | | `BINDING.WASM_COMMAND_ARG_MAP_FAILED` | Couldn't map value for argument $\{index} while executing command \`\{command}\` > \{message} | Argument types must match the command's declared signature; see the inner message for the failing field. | | | `BINDING.WASM_COMMAND_SINGLE_ARG_MISMATCH` | Received single argument for command \{command}. Expected \{expected}. | Pass exactly the number of arguments the command expects, as an array. | | | `BINDING.WASM_FIND_RESULT_MISSING_ASSETS` | Missing required field 'assets' in find assets result | Return an \`assets\` array (may be empty) from the asset source \`findAssets\` callback. | | | `BINDING.WASM_FIND_RESULT_MISSING_CURRENT_PAGE` | Missing required field 'currentPage' in find assets result | Return a \`currentPage\` integer from the asset source \`findAssets\` callback. | | | `BINDING.WASM_FIND_RESULT_MISSING_TOTAL` | Missing required field 'total' in find assets result | Return a \`total\` integer from the asset source \`findAssets\` callback. | | | `BINDING.WASM_FONT_MISSING_SUBFAMILY` | Missing required field 'subFamily' in font | Provide a \`subFamily\` string on each font entry. | | | `BINDING.WASM_FONT_MISSING_URI` | Missing required field 'uri' in font | Provide a \`uri\` string on each font entry. | | | `BINDING.WASM_GRADIENT_STOP_MISSING_STOP_VALUE` | Gradient color stop must have a stop value | Each gradient color stop needs a \`stop\` number between 0 and 1. | | | `BINDING.WASM_MEM_ALLOC_FAILED_BUFFER` | Failed to allocate memory for buffer \{bufferUri} | Reduce the buffer size or grow the WASM memory cap before allocating. | | | `BINDING.WASM_MEM_ALLOC_FAILED_HANDLE` | Failed to allocate memory for handle \{handle} | Reduce concurrent in-flight engine handles or grow the WASM memory cap. | | | `BINDING.WASM_PAYLOAD_COLOR_CMYK_MISSING_FIELDS` | \`payload.color\` with \`CMYK\` color space must have fields \`c\`, \`m\`, \`y\`, \`k\`. | Provide \`c\`, \`m\`, \`y\`, \`k\` numeric fields on \`payload.color\` when \`colorSpace\` is \`'CMYK'\`. | | | `BINDING.WASM_PAYLOAD_COLOR_MISSING_COLOR_SPACE` | \`payload.color\` must have \`colorSpace\` field. | Set \`payload.color.colorSpace\` to \`'sRGB'\`, \`'CMYK'\`, or \`'SpotColor'\`. | | | `BINDING.WASM_PAYLOAD_COLOR_SPOT_MISSING_FIELDS` | \`payload.color\` with \`SpotColor\` color space must have fields \`name\`, \`externalReference\`, \`representation\`. | Provide \`name\`, \`externalReference\`, and \`representation\` on a \`SpotColor\` payload color. | | | `BINDING.WASM_PAYLOAD_COLOR_SPOT_REPRESENTATION_INVALID` | A \`payload.color\` with colorSpace \`SpotColor\` must use a \`RGB\` or \`CMYK\` color for its \`representation\`. | Set \`representation.colorSpace\` to \`'sRGB'\` or \`'CMYK'\` on the SpotColor payload. | | | `BINDING.WASM_PAYLOAD_COLOR_SRGB_MISSING_FIELDS` | \`payload.color\` with \`sRGB\` color space must have fields \`r\`, \`g\`, and \`b\`. | Provide \`r\`, \`g\`, \`b\` numeric fields on \`payload.color\` when \`colorSpace\` is \`'sRGB'\`. | | | `BINDING.WASM_PROPERTIES_NOT_ARRAY` | 'properties' is not an array. | Pass \`properties\` as a JavaScript array of \`AssetProperty\` objects. | | | `BINDING.WASM_SOURCE_SET_NOT_ARRAY` | 'sourceSet' is not an array. | Pass \`sourceSet\` as a JavaScript array of source objects. | | | `BINDING.WASM_TRANSFORM_PRESET_FIXED_ASPECT_MISSING_FIELDS` | 'payload.transformPreset' of type 'FixedAspectRatio' must have fields 'width', and 'height'. | Provide numeric \`width\` and \`height\` fields on a \`'FixedAspectRatio'\` transformPreset. | | | `BINDING.WASM_TRANSFORM_PRESET_FIXED_SIZE_MISSING_FIELDS` | 'payload.transformPreset' of type 'FixedSize' must have fields 'width', 'height', and 'designUnit'. | Provide numeric \`width\`, \`height\`, and a \`designUnit\` string on a \`'FixedSize'\` transformPreset. | | | `BINDING.WASM_TRANSFORM_PRESET_MISSING_TYPE` | Missing required field 'type' in asset transformPreset | Set \`transformPreset.type\` to one of \`'FreeAspectRatio'\`, \`'FixedAspectRatio'\`, \`'ContentAspectRatio'\`, \`'FixedSize'\`. | | | `BINDING.WASM_TRANSFORM_PRESET_UNKNOWN_TYPE` | Unknown transformPreset type: \{type} | Use one of \`'FreeAspectRatio'\`, \`'FixedAspectRatio'\`, \`'ContentAspectRatio'\`, \`'FixedSize'\`. | | | `BINDING.WASM_TYPEFACE_MISSING_FONTS` | Missing required field 'fonts' in typeface | Provide a non-empty \`fonts\` array on the typeface object. | | | `BINDING.WASM_TYPEFACE_MISSING_NAME` | Missing required field 'name' in typeface | Provide a \`name\` string on the typeface object. | | | `BINDING.WASM_UNKNOWN_AUDIO_OUTPUT_TYPE` | Unknown audio output type: \{audioOutput} | Pass one of the documented audio-output type strings. | | | `BINDING.WASM_UNKNOWN_COLOR_SPACE` | Unknown color space: \{colorSpace} | Pass one of \`'sRGB'\`, \`'CMYK'\`, \`'SpotColor'\`. | | | `BINDING.WASM_UNKNOWN_CUTOUT_OPERATION` | Unknown CutoutOperation \{op} | Pass one of the documented \`CutoutOperation\` enum values. | | | `BINDING.WASM_UNKNOWN_CUTOUT_TYPE` | Unknown CutoutType \{type} | Pass one of the documented \`CutoutType\` enum values. | | | `BINDING.WASM_UNKNOWN_HORIZONTAL_ALIGNMENT` | Unknown horizontal alignment \{alignment}, use Left, Right, or Center | Pass \`'Left'\`, \`'Right'\`, or \`'Center'\`. | | | `BINDING.WASM_UNKNOWN_SCOPE_STATE` | Unknown GlobalScopeState \{value} | Pass one of the documented \`GlobalScopeState\` enum values. | | | `BINDING.WASM_UNKNOWN_VERTICAL_ALIGNMENT` | Unknown vertical alignment \{alignment}, use Top, Bottom, or Center | Pass \`'Top'\`, \`'Bottom'\`, or \`'Center'\`. | | ## BLOCK Design-block lifecycle, properties, hierarchy, animation. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `BLOCK.ALWAYS_ON_BOTTOM_UNSUPPORTED` | The block does not have an always-on-bottom property. | Only specific block types (e.g. scene, page) carry always-on-bottom. Check api.block.getType(block) is a supported container before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ALWAYS_ON_TOP_UNSUPPORTED` | The block does not have an always-on-top property. | Only specific block types (e.g. scene, page) carry always-on-top. Check api.block.getType(block) is a supported container before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_ASSET_MALFORMED_MODE_KEY` | Malformed asset. The mode should be stored in meta under the "mode" key. | Move the animation mode into asset.meta.mode. The current asset shape is rejected by the loader. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_ASSET_MALFORMED_TYPE_KEY` | Malformed asset. The animation type or "none" should be stored in meta under the "type" key. | Move the animation type into asset.meta.type. The value must be the type identifier or the literal string 'none'. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_ASSET_MISSING_MODE` | The asset does not contain a mode property. | Animation assets must declare a 'mode' field in their meta. Add the field to the asset definition. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_ASSET_MISSING_TYPE` | The asset does not contain a type property. | Animation assets must declare a 'type' field in their meta. Add the field to the asset definition. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_ASSET_MODE_MISMATCH` | The animation type \{type} cannot be used as a "\{mode}" animation. | Loop animation types need the mode "loop", and in and out animation types need "in" or "out". Change asset.meta.mode or asset.meta.type. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_ASSET_TEXT_ONLY` | The animation type \{type} can only be added to text blocks. | Text animations apply only to text blocks. Apply the asset to a text block, or pick an animation type that is not a text animation. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NOT_A_PAN_ANIMATION` | The selected animation is not a pan animation. | This API only applies to pan-type animations. Switch to a pan animation first or use the generic property setter. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NOT_A_SLIDE_ANIMATION` | The selected animation is not a slide animation. | This API only applies to slide-type animations. Switch to a slide animation first or use the generic property setter. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NOT_IN_TYPE` | The animation \{blockId} cannot be used as an "in" animation. | The animation block is not tagged as an in/out animation. Create an in/out animation via createAnimation with a compatible type. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NOT_LOOP_TYPE` | The animation \{blockId} cannot be used as a "loop" animation. | The animation block is not tagged as a loop animation. Create a loop animation via createAnimation with a compatible type. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NOT_OUT_TYPE` | The animation \{blockId} cannot be used as an "out" animation. | The animation block is not tagged as an in/out animation. Create an in/out animation via createAnimation with a compatible type. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NO_ANIMATIONS_ON_BLOCK` | The block does not have any animations. | No animation has been applied to this block. Add an animation asset before querying or modifying it. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NO_ANIMATION_TO_EDIT` | The selected block does not have an animation to be edited. | Apply an animation asset to the block before invoking edit APIs. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NO_EASING_PROPERTY` | The selected animation has no easing property. | The current animation type does not expose an easing setting. Verify the animation type supports easing before setting it. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NO_EASING_PROPERTY_ON_BLOCK` | The target block does not have an animationEasing property. | The current animation type doesn't expose easing. Pick a type that does, or skip the property. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NO_OVERLAP_PROPERTY` | The target block does not have an textAnimationOverlap property. | textAnimationOverlap only exists on text-animation types. Confirm the animation type first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NO_TEXT_WRITING_STYLE` | The selected animation has no text writing style property. | Text writing style is only available on text-animation types. Confirm the active animation supports it. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_NO_WRITING_STYLE_PROPERTY` | The target block does not have an textAnimationWritingStyle property. | textAnimationWritingStyle only exists on text-animation types. Confirm the animation type first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_TEXT_ONLY` | The animation \{blockId} can only be added to text blocks. | Text animations only apply to text blocks. Either pick a non-text animation or attach this animation to a text block. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_TYPE_NOT_REGISTERED` | Unknown animation type: \{type} | Animation type '\{type}' is not registered. Use createAnimation with a built-in type or register a custom animation first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_UNKNOWN_EASING` | Unknown easing value: \{value} | Easing '\{value}' is not recognized. Use one of the predefined easing names. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_UNKNOWN_ENUM` | Unknown enum value: \{value} | The enum value '\{value}' is not a member of the targeted animation property. Inspect the property's valid set via the schema. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_UNKNOWN_MODE` | Unknown animation mode: \{mode} | Animation mode '\{mode}' is not one of 'In', 'Loop', or 'Out'. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_UNKNOWN_MODE_IN_ASSET` | The asset contains an unknown mode. | The asset's 'mode' value is not recognized. Valid modes are 'In', 'Loop', and 'Out'. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_UNKNOWN_PROPERTY_TYPE` | Unknown property type. | The animation property type is not handled by the editor. This likely indicates a stale or unsupported asset definition. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_UNKNOWN_TYPE` | Unknown animation type. | The animation type identifier is not registered. Confirm the asset 'type' matches a built-in or registered custom animation. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ANIMATION_UNSUPPORTED` | The block does not support being animated. | Only blocks with animation capability accept animation assets. Call api.block.supportsAnimation(block) before applying an animation. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ASSET_SOURCE_SORT_KEY_MISSING` | Sort key not found in asset source. | The asset source does not provide the requested sort key. Use a sort key the source declares. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.AUDIO_TRACK_INDEX_OUT_OF_BOUNDS` | Audio track index \{index} is out of bounds. Valid range: 0-\{max}. | Pass an audio-track index in \[0, \{max}]. Use getAudioTrackCountFromVideo() to inspect the count. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.AUTO_TO_FREE_LAYOUT_UNSUPPORTED` | Switching from an automatic to free layout isn't supported yet. | Once a page or stack is in automatic layout, free-layout switching requires reconstructing the children. Re-create the layout instead. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.BACKGROUND_COLOR_UNSUPPORTED` | The block doesn't have a background color. | Only pages and a handful of container blocks expose a background color. Call api.block.supportsBackgroundColor(block) before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.BLEND_MODE_UNSUPPORTED` | The block doesn't have a blend mode. | Only renderable design blocks expose blendMode. Call api.block.supportsBlendMode(block) before setting it. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.BLOCKS_NOT_COMBINABLE` | Blocks cannot be combined. | Boolean combine requires two or more shape-bearing blocks. Verify the selection contains compatible shapes. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.BLUR_TYPE_UNSUPPORTED_ON_GROUP` | A group supports only a uniform blur, not \{type}. | Create the blur with createBlur('uniform'). Linear, radial and mirrored blurs apply only to blocks with a fill. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.BLUR_UNKNOWN_TYPE` | Unknown blur type: \{type} | Blur type '\{type}' is not registered. Use one of the built-in blur types (uniform, linear, radial, mirrored). | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.BLUR_UNSUPPORTED` | Target block doesn't support blur. | Only renderable design blocks carry blur. Verify with supportsBlur(block) before applying. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.BUFFER_LENGTH_OUT_OF_RANGE` | Length \{length} at offset \{offset} is out of range \[0, \{max}]. | Ensure offset + length \<= \{max}. Either reduce the length or grow the buffer first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.BUFFER_NOT_FOUND` | Buffer not found: \{uri} | No buffer is registered at URI '\{uri}'. Call createBuffer or write to the URI before reading. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.BUFFER_OFFSET_OUT_OF_RANGE` | Offset \{offset} is out of range \[0, \{max}]. | Pass an offset in \[0, \{max}]. Use the buffer's current size to validate before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.BUFFER_STAGING_FAILED` | The temporary storage of the buffer \{uri} failed. | A large buffer is kept in a temporary file. The disk could not hold the file, or the process could not map it. Free disk space, then create the buffer again and write all of its bytes. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.BUFFER_URI_INVALID` | Invalid buffer URI: \{uri} | Buffer URIs must use the 'buffer://' scheme. Re-create the buffer to obtain a valid URI. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CAMERA_DESTRUCTION_NOT_ALLOWED` | Destruction of camera is not allowed. | Cameras are managed by the scene. Destroy the scene, not the camera, to remove camera state. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CAMERA_TRANSFORM_LOCK` | Camera's transform cannot be locked. | Cameras are not lockable. Use scene-level interaction settings to restrict camera movement. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CAPTIONS_DISABLED` | Creation of captions is not allowed. Please enable the video captions feature. | Enable settings.features.videoCaptionsEnabled before creating a caption block. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CAPTION_TRACKS_DISABLED` | Creation of caption tracks is not allowed. Please enable the video captions feature. | Enable settings.features.videoCaptionsEnabled before creating a caption track block. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CHILDREN_INDEX_OUT_OF_BOUNDS` | Index \{index} is out of bounds for \{count} children. | Pass an index in \[0, \{count}]. Use getChildren(block).size() to inspect the current count. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.COLOR_COMPONENT_OUT_OF_RANGE` | \{component} color component out of range \[0, 1]: \{value}. | Clamp '\{component}' to the \[0, 1] range. NaN is also rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.COLOR_NOT_IN_COLOR_SPACE` | The color is not in the '\{colorSpace}' color space. | Read the color via the accessor for its actual color space, or convert it first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.COLOR_SPACE_CONVERSION_NOT_SUPPORTED` | Cannot convert to that color space. | Color-space conversion to SpotColor is not defined — convert to sRGB or CMYK instead. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.COLOR_TINT_OUT_OF_RANGE` | Tint out of range \[0, 1]: \{value}. | Clamp the tint to the \[0, 1] range. NaN is also rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.COMBINE_TEXT_FONT_LOADING` | Block \{id} is a text block whose fonts have not finished loading. Call forceLoadResources on it before combine. | Wait for the text block's fonts to load (api.block.forceLoadResources) before invoking boolean combine. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.COMPONENT_NOT_REGISTERED` | \{component} is not a registered type. | Reflection has no registration for component '\{component}'. Confirm the property string is correct. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.COMPONENT_NO_PROPERTY` | \{component} doesn't have property \{property}. | Component '\{component}' has no field named '\{property}'. Inspect findAllProperties(block) for valid keys. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CONTENT_ASPECT_IMAGE_NO_DIMENSIONS` | ContentAspectRatio: image fill has no usable intrinsic dimensions yet; the image is not decoded and the sourceSet is empty. | Set 'fill/image/sourceSet' with explicit width and height, or wait for the image to decode before applying the preset. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CONTENT_ASPECT_NOT_IMAGE_OR_VIDEO` | ContentAspectRatio: fill is neither an image nor a video; only those carry intrinsic dimensions. | Replace the fill with an image or video fill. Color and gradient fills have no intrinsic dimensions. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CONTENT_ASPECT_NO_FILL` | ContentAspectRatio: block has no fill; only image and video fills carry intrinsic dimensions. | Add an image or video fill to the block before querying its content aspect ratio. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CONTENT_ASPECT_VIDEO_NO_DIMENSIONS` | ContentAspectRatio: video fill has no usable intrinsic dimensions yet; wait for the first frame to decode or set the sourceSet with explicit width and height. | Wait for the first frame to decode, or set 'fill/video/sourceSet' with explicit width and height before applying the preset. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CONTENT_FILL_ALIGNMENT_UNSUPPORTED` | The block doesn't support content fill alignment. | Only blocks with a sized image/video fill support contentFillAlignment. Resolve the fill with api.block.getFill(block) and confirm api.block.getType(fill) is an image or video fill first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CONTENT_FILL_MODE_UNSUPPORTED` | The block doesn't have a content fill mode. | Only blocks with image/video fills expose a content fill mode. Call api.block.supportsContentFillMode(block) before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CREATION_NOT_ALLOWED` | Creation of \{type} is not allowed. | Type '\{type}' is not user-creatable. It is created implicitly by other engine operations. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CUTOUT_NO_BLOCK_SELECTED` | No block selected can be given a cutout. | Cutouts apply to selected graphic blocks. Select at least one cutout-capable block first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.CUTOUT_PATH_REQUIRED` | 'vectorPath' is required for Cutout. | Pass a non-empty 'vectorPath' string when creating a Cutout block. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.DIFFERENCE_NO_EFFECT` | Difference has no visible effect. | The subtracted shape does not intersect the base. Reposition or resize so they overlap. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.DROP_SHADOWS_UNSUPPORTED` | The block doesn't support drop shadows. | Only renderable design blocks carry shadow properties. Call api.block.supportsDropShadow(block) before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.EFFECTS_UNSUPPORTED` | Target block doesn't support effects. | Only renderable design blocks carry an effects stack. Verify with supportsEffects(block) before mutating it. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.EFFECT_INDEX_OUT_OF_BOUNDS` | Index \{index} out of bounds (\{count}). | Pass an index in \[0, \{count}]. Use getEffects(block).size() to inspect the current count. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.EFFECT_NOT_AN_EFFECT` | Only effects can be enabled and disabled. Use setVisible instead if you are trying to change the visibility of a design block. | Pass an effect block, or use setVisible(block, ...) to toggle a design block's visibility. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.EFFECT_SOURCE_INVALID` | Blocks of type \{type} cannot be an effect source. | Pass a graphic, text or group block as the effect source. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.EFFECT_SOURCE_UNSUPPORTED` | Blocks of type \{type} do not read an effect source. | Only the displacement map effect reads a source block. Pass an effect of that type. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.EFFECT_UNKNOWN_TYPE` | Unknown effect type: \{type} | Effect type '\{type}' is not registered. Use a built-in effect type or register a custom one before creating. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ELEMENTS_NOT_ALIGNABLE` | Elements cannot be aligned. | Alignment requires two or more selectable design blocks under a common parent that supports free layout. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ELEMENTS_NOT_DISTRIBUTABLE` | Elements cannot be distributed. | Distribution requires three or more selectable design blocks under a common parent that supports free layout. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ELEMENTS_NOT_GROUPABLE` | Elements cannot be grouped. | Grouping requires two or more design blocks under the same parent. Verify the selection before calling group(). | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ENTITY_NOT_LAID_OUT` | Entity \{block} has not been laid out yet. | Call api.scene.update() (or wait for the next frame) so layout has run, then retry. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.ENUM_VALUE_INVALID` | Invalid enum value \{value} for property \{property}. | Use one of the values returned by api.block.getEnumValues('\{property}'). | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.EXPORTABLE_UNSUPPORTED` | The block doesn't have exportable option. | Only specific block types track the 'exportable' flag. Check api.block.getType(block) before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.FILL_ALREADY_USED_BY_BLOCK` | The fill block is already used by this block. | A block cannot use one fill block as both its fill and its stroke fill. Duplicate the fill block and pass the copy. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.FILL_GET_SOLID_COLOR_WRONG_TYPE` | Tried to get solid color fill value on a block that has no such fill color. | The block's fill is not a solid color. Inspect the fill type via getType(getFill(block)) first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.FILL_MISSING` | Target block has no Fill. | Attach a fill via setFill(block, fill) or use supportsFill(block) to confirm support. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.FILL_NOT_VALID` | Specified fill is not a valid fill. | Create the fill via api.block.createFill(...) and pass that id. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.FILL_NO_SOLID_COLOR` | Fill has no solid color. | The fill block is not a SolidColor. Inspect getType(getFill(block)) and use the matching accessor. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.FILL_NO_SOLID_COLOR_FILL` | Block has no solid color fill. | Attach a solid-color fill via setFill(block, createFill('color')) before reading the color. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.FILL_SET_SOLID_COLOR_WRONG_TYPE` | Tried to set Fill Color on a block with different fill type. | Switch the block's fill to a solid-color fill, or use the matching API for the current fill type. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.FILL_TEXT_SOLID_COLOR_ONLY` | Text blocks only support solid color fills. | Use createFill('color') for text blocks. Image, video, and gradient fills are not supported on text. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.FILL_UNSUPPORTED` | The block doesn't have a fill. | Only renderable design blocks expose a fill. Confirm the block type before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.GRADIENT_COLOR_STOPS_DUPLICATE` | Gradient color stops need to have unique values between 0 and 1. Found duplicate value \{value}. | Remove the duplicate stop at \{value} so each stop's position is unique. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.GRADIENT_COLOR_STOPS_NOT_SORTED` | Gradient color stops must be sorted in ascending order. | Sort the stops by position ascending before passing the array. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.GRADIENT_COLOR_STOPS_OUT_OF_RANGE` | Gradient color stops must be between 0 and 1. | Clamp each stop's position into \[0, 1] before passing the array to api.block.setGradientColorStops; positions outside the range are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.GRADIENT_COLOR_UNSUPPORTED` | The block does not have a gradient color property. | Only gradient-fill blocks expose gradient color stops. Resolve the fill with api.block.getFill(block) and confirm api.block.getType(fill) is a gradient fill first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.GROUP_ABSOLUTE_ONLY` | Block is a group and can only have Absolute position mode. | Groups always use absolute layout. Don't set a relative position mode on a group block. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.HEIGHT_INVALID_FOR_SCALING` | Current height is invalid for scaling. | The block's current height is zero or non-finite. Set a valid height before scaling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.HISTORY_HANDLE_INVALID_AT` | \{handle} is not a valid history handle. | The history handle is unknown or has been released. Re-obtain it via createHistory() before calling. | [Undo And History](https://img.ly/docs/cesdk/mac-catalyst/concepts/undo-and-history-99479d/) | | `BLOCK.ID_INVALID` | The block with ID \{block} is not valid. It may have been deleted, e.g., when a new scene was loaded. | Re-resolve the block id after scene changes. Use isValid(block) to guard before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.IMAGE_FILL_UNSUPPORTED` | The block does not have an image fill property. | Only blocks with an image fill expose this property. Resolve the fill with api.block.getFill(block) and confirm api.block.getType(fill) is an image fill before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.IMAGE_LOAD_FAILED` | Failed to load image. | The image source could not be decoded or fetched. Verify the URI is reachable, the MIME type is one the engine supports, and the bytes are a valid image. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_DUPLICATE_TIME` | Two keyframes have the time \{time}. | Each keyframe of a track needs its own time. Remove or move one of them. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_EASING_OUT_OF_RANGE` | The bezier easing handle has the time value \{value}, which is outside the range 0 to 1. | Keep the x values of both bezier handles between 0 and 1, as in CSS cubic-bezier. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_EMPTY` | The keyframe list is empty. | Pass at least one keyframe. Use removeKeyframes(block, property) to remove a track. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_ENUM_OUT_OF_RANGE` | The value '\{value}' is not a valid \{name}. | Pass one of the documented values for the easing type, the easing preset and the track mode. A discrete number must be a whole 32-bit number. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_MIXED_COLOR_SPACES` | The color keyframes of one track use different color spaces or spot colors. | Use one color space for all color keyframes of a track. For spot colors, use one spot color name and animate the tint. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_MIXED_VALUE_KINDS` | The keyframes of one track have values of different kinds. | All keyframes of a track must hold the same kind of value. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_NO_TRACK` | The property '\{property}' of this block has no keyframes. | Call getAnimatedProperties(block) to list the properties that have keyframes. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_PROPERTY_NOT_ANIMATABLE` | The property '\{property}' of this block cannot have keyframes. | Call isPropertyAnimatable(block, property) first. Only listed properties of the block that the engine marks as animatable accept keyframes. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_RELATIVE_UNSUPPORTED` | A relative track cannot hold \{kind} values. | Use the Relative track mode only for float and vec2 properties. Use Absolute for all others. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_SCALE_AXES_DIFFER` | The transform/scale/x and transform/scale/y keyframes differ in time, easing or mode, so they cannot be read as one transform/scale track. | Read transform/scale/x and transform/scale/y separately. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_SPATIAL_TANGENT_UNSUPPORTED` | A keyframe with a \{kind} value cannot have spatial tangents. | Spatial tangents describe a motion path. Set them only on keyframes with vec2 values. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_TIME_OUT_OF_RANGE` | The keyframe time \{time} is outside the range \[0, 1]. | Keyframe times are normalized over the block's duration. Pass finite times from 0 to 1. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEYFRAMES_VALUE_KIND_MISMATCH` | The property '\{property}' takes \{expected} keyframe values, not \{actual}. | Pass values of the property's kind. A discrete property also needs the same type: bool, int or string. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.KEY_NOT_FOUND` | Key \{key} on block \{block} not found. | Property '\{key}' is not defined on block \{block}. Use findAllProperties(block) to enumerate valid keys. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.MASKS_UNSUPPORTED` | Blocks of type \{type} don't support masks. | Only graphic, text and group blocks carry a mask stack. Verify with supportsMasks(block) before changing it. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.MASK_INDEX_OUT_OF_BOUNDS` | Mask index \{index} out of bounds (\{count}). | Pass an index in \[0, \{count}] to insert, or below \{count} to remove. Use getMasks(block) to inspect the current count. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.MASK_INVALID` | The block is not a mask. | Create a mask with createMask() and pass that block. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.MATTE_SOURCE_INVALID` | A block cannot use itself, one of its parents, a block that it or one of its parents masks, a block that contains such a block, or a block on another page as its matte. | Pass a matte source on the same page that does not contain the block and is not masked by the block or its parents. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.MATTE_UNSUPPORTED` | Blocks of type \{type} cannot have or be a matte. | Only graphic, text and group blocks support mattes. Pass one of those block types on both sides. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.MISSING_REQUESTED_COMPONENT` | The block doesn't have the requested properties. | The requested component is not attached to this block. Use the matching api.block.supports\*(block) query (or check api.block.getType(block)) to confirm the feature before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.MODIFIERS_UNSUPPORTED` | Target block doesn't support modifiers. | Only shape blocks carry a modifier stack. Verify with supportsModifiers(block) before mutating it. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.MODIFIER_INDEX_OUT_OF_BOUNDS` | Index \{index} out of bounds (\{count}). | Pass an index in \[0, \{count}]. Use getModifiers(block).size() to inspect the current count. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.MODIFIER_NOT_A_MODIFIER` | The block to add is not a modifier. | Create the block with createModifier(type) before adding it to a shape. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.MODIFIER_UNKNOWN_TYPE` | Unknown modifier type: \{type} | Modifier type '\{type}' is not registered. Use a built-in modifier type such as 'trim'. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.MOTION_BLUR_VALUE_OUT_OF_RANGE` | The value \{value} for '\{property}' is outside the range \[\{min}, \{max}]. | Pass a motion blur setting inside its documented range. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NAME_UNSUPPORTED` | The block does not have a name. | Only renderable design blocks carry a name. Check api.block.getType(block) is a design block before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_ATTACHED_TO_SCENE` | Block \{block} is not attached to a scene. | Append the block under the active scene (or one of its descendants) before calling this API. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_A_CUTOUT_BLOCK` | The block \{id} is not a Cutout block. | This API requires a cutout block. Use createCutout or filter by '//ly.img.ubq/cutout' first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_A_DESIGN_BLOCK` | Block is not a design block. | This API requires a design block (page, graphic, text, audio, video, etc.). Fills, shapes, and animations are not design blocks. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_A_TEXT_BLOCK` | Entity \{block} is not a text block. | Pass a text block id. Use getType(block) to confirm the block type before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_A_TEXT_BLOCK_SIMPLE` | block is not a text block. | Pass a text block. Use getType(block) to confirm the block type before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_A_VIDEO_FILL_BLOCK` | The provided block must be a video fill block or a block with a video fill. | Pass a video fill block directly, or a design block whose fill is a video. Use getFill(block) to resolve. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_KNOWN_BLOCK_TYPE` | \{type} is not a known block type. | Block type '\{type}' is not registered. Use findAllTypes() to enumerate supported types. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_LAID_OUT` | Block hasn't been laid out yet. | Call api.scene.update() (or wait for the next frame) so layout has run, then retry. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_LAID_OUT_AABB` | Could not get AABB. Block hasn't been laid out yet. | Layout has not run for this block. Trigger a layout pass or wait one frame before querying the AABB. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_LAID_OUT_BEFORE_ADJUST_CROP` | Block has not been laid out yet. Call update() before adjustCropToFillFrame(). | adjustCropToFillFrame() requires a laid-out frame. Run api.scene.update() first, then retry. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_LAID_OUT_FLIP_H` | Could not flip horizontally. Block hasn't been laid out yet. | Layout has not run for this block. Trigger a layout pass or wait one frame before flipping. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_LAID_OUT_FLIP_V` | Could not flip vertically. Block hasn't been laid out yet. | Layout has not run for this block. Trigger a layout pass or wait one frame before flipping. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_LAID_OUT_FOR_SCENE` | The block \{id} has not been layouted and may therefore not be part of the scene. | Confirm block \{id} is attached and the scene has been updated. Trigger an explicit update if the block was just added. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_LAID_OUT_ROTATION` | Could not set rotation. Block hasn't been laid out yet. | Layout has not run for this block. Trigger a layout pass or wait one frame before setting rotation. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_LAID_OUT_SCALE` | Could not scale. Block hasn't been laid out yet. | Layout has not run for this block. Trigger a layout pass or wait one frame before scaling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NOT_VALID` | Block is not valid. | The block id no longer references a live block. It may have been destroyed; re-resolve the id before calling this API. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NO_AUDIO_TRACKS_FOUND` | No audio tracks found in the video. | The video container reports zero audio tracks. Use a source that includes audio or skip this operation. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NO_AUDIO_TRACKS_IN_VIDEO` | The video does not contain any audio tracks. | The decoded video has no audio streams. Confirm the source file actually contains audio. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NO_BLOCK_SELECTED` | No block is selected. | This API requires a selected block. Use api.block.findAllSelected() to confirm a selection before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NO_KIND` | Block does not have a kind. | Only design blocks expose a 'kind' property. Check api.block.getType(block) to confirm it is a design block (not a fill/shape/animation) before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NO_PARENT` | Block doesn't have a parent. | The block is detached (e.g. the scene root). Attach it to a parent or guard with hasParent(block) before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NO_POSITION` | Block has no defined position. | Position requires a Position component. Auto-layout containers or unattached blocks may not expose one. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NO_SHAPE_PROPERTY` | Target block has no shape property. | Only blocks with a shape component (graphic, vector) expose a shape property. Call api.block.supportsShape(block) before accessing the shape. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.NO_SIZE` | Block has no defined size. | Size requires a Size component. Confirm the block is a sized type (page, graphic, text) before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.OPACITY_UNSUPPORTED` | The block doesn't have an opacity. | Only renderable design blocks expose opacity. Call api.block.supportsOpacity(block) before setting it. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.OPERATION_PRECONDITION_FAILED` | \{reason} | The structural precondition check returned a free-text reason — see \`reason\` for the specific blocker (e.g. mixed parents, incompatible block types, missing layout). | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.OP_NEEDS_TWO_BLOCKS` | Not enough blocks to perform operation. Must be at least two. | Select at least two blocks before invoking this operation. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PARENT_NOT_LAID_OUT` | Parent block hasn't been laid out yet. | Layout has not run for the parent. Trigger an update or wait one frame before reading relative geometry. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PATH_OPERATION_FAILED` | The boolean operation could not include the shape of block \{id}. | The shape of block \{id} is too complex or too large for the boolean operation. Simplify the shape, or leave the block out. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PENDING_PROGRESS_INVALID` | Pending progress must be between 0 and 1. | Clamp the progress value to \[0, 1] before passing it to setPendingProgress. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.POSITION_LOCKED` | Block's position is locked and can't be modified. | Call setPositionLocked(block, false) before changing the position. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.POSITION_PARENT_CONTROLLED` | Block's position is controlled by the parent block and can't be modified. | The block's parent uses an automatic layout. Detach the block, switch the parent to free layout, or modify the parent instead. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_BOUND` | Property "\{property}" is bound to variable "\{variable}". | Set the variable instead, or remove the binding with removeVariableBinding first. | | | `BLOCK.PROPERTY_ENUM_CAST_FAILED` | \{property} cannot be cast to an enum value | The property value does not convert to an unsigned 32-bit integer. The property may not actually be an enum type, or its reflected representation is misconfigured. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_ENUM_MEMBER_CAST_FAILED` | \{member} cannot be cast to an enum value | An enum member's value does not convert to an unsigned 32-bit integer. The reflected enum is misconfigured. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_FONT_FILE_URI_DIRECT_UNSUPPORTED` | Setting the "text/fontFileUri" property directly is unsupported. Use the setFont API instead. | Call setFont(block, fontFileUri, typefaceName) so the typeface, weight and style stay consistent. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_GETTER_MISMATCH` | Incorrect function used, expected get\{kind}. | The property's type is '\{kind}'. Call the matching get\{kind}(...) accessor instead. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_INVALID_ENUM_VALUE` | \{property} holds an invalid enum value. | The property's integer value does not match any registered enum member. The block's stored state may be corrupt. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_INVALID_ENUM_VALUE_CHOICES` | Invalid enum value, expected one of: \[\{choices}] | Pass one of the listed enum identifiers. The catalog \`choices\` arg carries the comma-separated quoted list the setter would have accepted. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_NOT_AN_ENUMERATION` | Property's type is not an enumeration. | Querying the allowed enum choices is only valid for enum-typed properties. Check the property's \`PropertyType\` first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_NOT_ENUM` | \{property} is not an enum property. | \`getPropertyAllowedValues(...)\` / enum accessors only apply to enum-typed properties. Check the property type first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_NOT_FOUND` | Property not found: "\{property}". | Inspect api.block.findAllProperties(block) for the set of properties this block exposes. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_NOT_FOUND_WITH_HINT` | \{prefix}Valid properties, as listed by \`findAllProperties(id)\`, are: \{properties}. | Pick one of the listed properties or call findAllProperties(block) to inspect the live set. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_NOT_READABLE` | Property is not readable. | This property is write-only on the requested block type. Use the matching setter instead. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_NOT_WRITEABLE` | Property is not writeable. | This property is read-only on the requested block type. Inspect the value with the matching getter instead. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.PROPERTY_SETTER_MISMATCH` | Incorrect function used, expected set\{kind}. | The property's type is '\{kind}'. Call the matching set\{kind}(...) accessor instead. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.RESULT_EMPTY_SHAPE` | Result is an empty shape. | The boolean operation produced no visible geometry. Adjust the inputs so they overlap appropriately. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SCENE_CREATE_DIFFERENT` | To create a scene, use \`createScene\` instead. | Scenes have a dedicated lifecycle. Call api.scene.create() rather than block.create('//ly.img.ubq/scene'). | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `BLOCK.SCOPES_UNSUPPORTED` | Block \{block} does not support scopes. | Scope APIs require a block type that carries access control. Most renderable design blocks do; pages and tracks typically do not. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SCOPE_INVALID` | Invalid scope: \{scope} | The scope name '\{scope}' is not recognized. Use one of the documented scope keys (e.g. 'design/style', 'editor/select'). | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SCOPE_MIXED_VALUES` | Not all underlying scopes of \{scope} have the same value. | The compound scope '\{scope}' aggregates multiple keys that currently disagree. Set them individually or accept the indeterminate state. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SCOPE_PERMISSION_DENIED` | \{message} | Unlock the scope '\{scope}' via setScopeEnabled(block, '\{scope}', true) before invoking this API. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SELECTION_DISABLED` | Selection disabled for block. | The block's selectability has been disabled via setSelectable(block, false). Re-enable to select it programmatically. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SHADOW_X_BLUR_INVALID` | The x-blur radius must be a finite number greater than or equal to 0. | Pass a non-negative finite x-blur. NaN and negative values are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SHADOW_X_OFFSET_INVALID` | The x-offset must be a finite number. | Pass a finite x-offset. NaN and ±infinity are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SHADOW_Y_BLUR_INVALID` | The y-blur radius must be a finite number greater than or equal to 0. | Pass a non-negative finite y-blur. NaN and negative values are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SHADOW_Y_OFFSET_INVALID` | The y-offset must be a finite number. | Pass a finite y-offset. NaN and ±infinity are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SHAPE_NOT_VALID` | Specified shape is not a valid shape. | The provided block id does not reference a shape sub-block. Use createShape or pick a registered shape type. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SOME_ELEMENTS_NOT_LOADED` | Some elements are not completely loaded. | Wait for the scene's resources to finish loading. Subscribe to resource events or poll api.scene.loadingState. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SOURCE_SET_EMPTY` | The source set is empty. | Populate the source set via addSource(...) before invoking source-set APIs. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.STROKES_UNSUPPORTED` | The block doesn't support strokes. | Only renderable design blocks carry stroke properties. Verify with supportsStroke(block) before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.STROKE_DASH_ARRAY_INVALID` | Stroke dash array values must be finite numbers. | Replace any NaN/infinity entries in the dash array with finite numbers. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.STROKE_DASH_OFFSET_INVALID` | Stroke dash offset must be a finite number. | Pass a finite numeric dash offset. NaN and ±infinity are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.STROKE_FILL_TYPE_UNSUPPORTED` | This fill type cannot paint a stroke. | Pass a color, gradient or stripe fill. Image, video and pixel stream fills cannot paint a stroke. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.STROKE_FILL_UNSUPPORTED` | This block's stroke cannot be painted with a fill block. | Only graphic blocks and pages paint their stroke with a fill. Use api.block.setStrokeColor() on other blocks. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.STROKE_MISSING` | Target block has no Stroke. | Enable the stroke via setStrokeEnabled(block, true) before mutating stroke properties. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.STROKE_MITER_LIMIT_INVALID` | The stroke miter limit must be a finite number greater than or equal to 1. | Pass a finite miter limit of 1 or more. NaN, infinity and values below 1 are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.STROKE_WIDTH_INVALID` | The stroke width must be a finite number greater than or equal to 0. | Pass a non-negative finite width. NaN and negative values are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SUBSHAPES_UNSUPPORTED` | The block is not a compound shape, so it has no subshapes. | Create the block with createShape('compound') before you add, read or remove subshapes. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SUBSHAPE_CYCLE` | A compound shape cannot contain itself. | Do not add a compound shape to itself or to one of its own subshapes. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SUBSHAPE_INDEX_OUT_OF_BOUNDS` | Index \{index} out of bounds (\{count}). | Pass an index in \[0, \{count}] to insert and in \[0, \{count}) to remove. Use getSubshapes(block) to inspect the current count. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SUBSHAPE_NOT_A_SHAPE` | Only shape blocks can be subshapes of a compound shape. | Create the subshape with createShape(type). | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SUBSHAPE_TOO_DEEP` | A shape tree can nest at most \{limit} shapes, from its top shape to its deepest subshape. | Insert the subshape into a compound shape nearer to the top, or into a shape tree with fewer levels. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SUBSHAPE_TYPE_UNSUPPORTED` | A line shape cannot be a subshape of a compound shape. | Use a vector path shape with two points instead. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.SVG_PATH_PARSE_FAILED` | The SVG path could not be parsed. | Pass a valid SVG path string (e.g. "M10 10 L20 20 Z"). Validate with an SVG path parser if unsure. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TARGET_NOT_AN_ANIMATION` | The target block is not an animation. | Pass an animation block id. Use getType(block) to confirm before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TARGET_NOT_A_VIDEO_FILL` | The target block is not a video fill. | Pass a video fill block. Use getFill(block) on a design block to resolve it. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_ANIMATORS_UNSUPPORTED` | Target block doesn't support text animators. | Only text and caption blocks have text animators. Verify with supportsTextAnimators(block) first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_ANIMATOR_ALREADY_ATTACHED` | The text animator already belongs to a block. | Remove it from its block first, or create a new text animator with createTextAnimator(). | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_ANIMATOR_INDEX_OUT_OF_BOUNDS` | Index \{index} out of bounds (\{count}). | Pass an index in \[0, \{count}]. Use getTextAnimators(block).size() to inspect the current count. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_ANIMATOR_NOT_AN_ANIMATOR` | The block to add is not a text animator. | Create the block with createTextAnimator() before adding it to a text block. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_CANNOT_TOGGLE_BOLD` | The block cannot be toggled between bold and normal font weights. | The current typeface does not expose both regular and bold weights. Pick a typeface that does. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_CANNOT_TOGGLE_ITALIC` | The block cannot be toggled between italic and normal font styles. | The current typeface does not expose both normal and italic styles. Pick a typeface that does. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_INVALID_BASELINE_SHIFT` | Baseline shift must be a finite number. | Pass a finite baseline shift in design units. NaN and ±infinity are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_INVALID_FONT_SIZE` | The font size must be a positive finite number. | Pass a positive finite font size. NaN, zero, and negative values are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_INVALID_KERNING` | Kerning must be a finite number. | Pass a finite kerning value. NaN and ±infinity are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_INVALID_LINE_INDEX` | Invalid line index: \{lineIndex}. | Pass a line index in \[0, lineCount(block) - 1]. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_INVALID_RANGE_FOR_LINE` | Invalid text range for line index: \{lineIndex}. | Wait for layout to settle (api.scene.update()) before requesting text ranges for a line. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_LINE_BOUNDS_FAILED` | Failed to calculate line bounds for line index: \{lineIndex}. | Ensure the block has been laid out (api.scene.update()) before requesting line bounds. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_LINE_HEIGHT_INVALID` | lineHeight must be greater than zero. | Pass a positive lineHeight. Zero and negative values are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_LIST_LEVEL_NEGATIVE` | The list level must be non-negative. | Pass a list level in \[0, 4]. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_LIST_LEVEL_TOO_LARGE` | The list level must be less than 5. | Pass a list level in \[0, 4]. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_NO_BLOCK_BEING_EDITED` | No text block is currently being edited. | Enter text editing mode (e.g. via setEditMode('Text')) before calling cursor/composition APIs. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_NO_TYPEFACE` | block has no typeface. | Assign a typeface to the block via setFont() before invoking this API. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_NO_TYPEFACE_AND_DEFAULT_NOT_REGISTERED` | block has no typeface set and the default font is not registered as a typeface. | Either set a typeface explicitly via setFont() or register the default font as a typeface. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_ON_PATH_INVALID_SVG_PATH` | svgPath is not a valid SVG path string. | Pass a non-empty, parseable SVG path (e.g. 'M 0,0 L 100,0'). Validate the string before calling setTextOnPath. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_ON_PATH_MULTIPLE_SUBPATHS` | svgPath must contain exactly one subpath (no multiple 'M' commands). | Text on path follows a single contour. Split the path or pass only one subpath (a single leading 'M'). | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_ON_PATH_NO_MEASURABLE_CONTOUR` | svgPath contains no measurable contour. | The path has zero length (e.g. a single 'M' with no draw commands). Provide a path with a real, measurable contour. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_PARAGRAPH_INDEX_NEGATIVE` | paragraphIndex must be non-negative. | Use a paragraph index >= 0. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_PARAGRAPH_INDEX_OUT_OF_RANGE` | paragraphIndex is out of range. | Pass a paragraph index in \[0, paragraphCount(block) - 1]. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_RANGE_FROM_OUT_OF_RANGE` | The \`from\` index is out of range. | Clamp 'from' to a non-negative index within the block's character count. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_RANGE_INVALID_ORDER` | Invalid range: from (\{from}) cannot be greater than to (\{to}). | Swap or adjust the bounds so 'from' \<= 'to'. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_RANGE_NEGATIVE` | Invalid range: from (\{from}) and to (\{to}) must be -1 or non-negative. | Use -1 to signal 'end of text' or pass a non-negative index for either bound. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_RANGE_TO_OUT_OF_RANGE` | The \`to\` index is out of range. | Clamp 'to' to an index within the block's character count. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_TYPEFACE_UPDATE_FAILED` | Failed to update text typeface: \{reason} | Inspect \`reason\` for the underlying text-shaping failure (typically font fallback, glyph coverage, or asset registry issues). | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_UNKNOWN_TYPEFACE` | block has an unknown typeface: '\{typeface}'. | Register the typeface '\{typeface}' with the engine, or pick one that has already been registered. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_UNSUPPORTED_FONT_STYLE` | The block does not support the given style. Failed to find a font with the specified style. | Pick a style that the current typeface supports. Inspect getTextFontStyles(block) for the available set. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TEXT_UNSUPPORTED_FONT_WEIGHT` | The block does not support the given weight. Failed to find a font with the specified weight. | Pick a weight that the current typeface supports. Inspect getTextFontWeights(block) for the available set. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.THRESHOLD_NOT_FINITE` | The threshold values must be a finite number. | NaN and infinity are not accepted as threshold inputs. Pass finite floating-point values. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TRANSFORM_LOCKED_ANCHOR_OR_SKEW` | Block's transform is locked and its anchor or skew can't be changed. | Call setTransformLocked(block, false) before changing the anchor or skew. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TRANSFORM_LOCKED_FILL_PARENT` | Block's transform is locked and can't fill its parent. | Call setTransformLocked(block, false) before invoking fillParent. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TRANSFORM_LOCKED_FLIP` | Block's transform is locked and can't be flipped. | Call setTransformLocked(block, false) before flipping. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TRANSFORM_LOCKED_RESIZE` | Block's transform is locked and can't be resized. | Call setTransformLocked(block, false) before resizing, or pick a different block. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TRANSFORM_LOCKED_ROTATE` | Block's transform is locked and can't be rotated. | Call setTransformLocked(block, false) before rotating. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TRANSFORM_LOCKED_SCALE` | Block's transform is locked and can't be scaled. | Call setTransformLocked(block, false) before scaling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TRANSFORM_PARENT_CYCLE` | The transform parent would make the block follow its own transform. | Pass a parent that is not the block itself and does not follow the block, directly or through other blocks. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TRANSFORM_PARENT_UNSUPPORTED` | Blocks of type \{type} cannot have or be a transform parent. | Only graphic and text blocks can follow the transform of another block. Pass a graphic or text block on both sides. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TRANSITION_BLOCK_INVALID` | Block \{blockId} is not a valid transition. | The block passed to setTransition does not exist or has no block type. Create the transition via createTransition first. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TRANSITION_BLOCK_NOT_A_TRANSITION` | Block \{blockId} is not a transition. | The block passed to setTransition is not a transition block. Create one via createTransition and pass that instead. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TRANSITION_TYPE_NOT_REGISTERED` | Unknown transition type: \{type} | Transition type '\{type}' is not registered. Use createTransition with one of the built-in transition types. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_CANNOT_AUTO_ORIENT` | Object of type \{type} cannot auto-orient. | Only graphic, text and caption blocks can follow the direction of their position keyframes. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_CANNOT_BE_ARRANGED` | Object of type \{type} cannot be arranged. | Arrange operations (bringToFront, etc.) apply only to design blocks within a container. Type \{type} doesn't qualify. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_CANNOT_BE_CLIPPED` | Object of type \{type} cannot be clipped. | Type \{type} has no clipping support. Wrap it in a group or page and apply clipping there. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_CANNOT_BE_FLIPPED` | Object of type \{type} cannot be flipped. | Type \{type} has no flip capability. Apply the flip to its parent or wrap in a group. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_CANNOT_BE_LOCKED` | Object of type \{type} cannot be locked. | Type \{type} has no lock capability. Locking only applies to design blocks. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_CANNOT_BE_PLACEHOLDER` | Object of type \{type} cannot be a placeholder. | Placeholders only apply to design blocks with content (graphic, text). Type \{type} cannot host placeholder behavior. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_CANNOT_BE_SELECTED` | Object of type \{type} cannot be selected. | Type \{type} is not user-selectable. Use a different selection target. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_CANNOT_HAVE_ANCHOR_OR_SKEW` | Object of type \{type} cannot have an anchor or a skew. | Only graphic, text and caption blocks support an anchor and a skew. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_CANNOT_HAVE_ROTATION` | Object of type \{type} cannot have a rotation. | Type \{type} does not support rotation. Wrap it in a group or rotate its parent instead. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_CANNOT_HAVE_TRANSFORM_SCALE` | Object of type \{type} cannot have a transform scale. | Only graphic, text and caption blocks support transform/scale/x and transform/scale/y. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NOT_A_CHILD` | Cannot add block of type \{type} as a child of another block. | Blocks of type \{type} are render blocks and must be attached via their dedicated property (e.g. fill, shape) rather than as a generic child. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_ADDING_CHILDREN` | Cannot add children to a block of type \{type}. | Blocks of type \{type} are not containers. Use a page, group, or track block to compose children. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_CHILDREN` | Object of type \{type} cannot have children. | Blocks of type \{type} are leaves in the scene tree. Use a container type (page, group, track) to hold children. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_FRAME` | Object of type \{type} does not have a frame. | Only design blocks with computed layout expose a frame. Cameras, scenes, fills, and shapes do not. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_GROUPS` | Object of type \{type} does not support groups. | Type \{type} cannot be grouped. Only design blocks under a common parent can be grouped. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_HIGHLIGHTING` | Object of type \{type} does not support highlighting. | Highlighting only applies to design blocks. Type \{type} cannot be highlighted. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_PARENT` | Object of type \{type} cannot have a parent. | Blocks of type \{type} are render blocks (fills, shapes, blurs, effects). They live alongside design blocks rather than under them. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_PLACEHOLDER_BEHAVIOR` | Object of type \{type} does not support placeholder behavior. | Type \{type} cannot be configured with placeholder hints or controls. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_PLACEHOLDER_CONTROLS` | Object of type \{type} does not have placeholder controls. | Type \{type} has no placeholder-related properties. Choose a design block that supports placeholders (graphic, text). | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_POSITION` | Object of type \{type} cannot have a position. | Blocks of type \{type} have no position property. Render blocks and scenes are positioned implicitly. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_ROTATION` | Object of type \{type} does not have a rotation. | Rotation requires a Rotation component, which is not present on type \{type}. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_SIZE` | Object of type \{type} cannot have a size. | Blocks of type \{type} have no size property. Cameras, scenes, fills, and shapes are sized implicitly via other blocks. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_NO_VISIBILITY_STATE` | Object of type \{type} does not have any visibility state. | Type \{type} is always visible (cameras, scenes). Use show/hide on a design block instead. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.TYPE_PERMANENTLY_NON_SELECTABLE` | Object of type \{type} is permanently non-selectable. | Type \{type} is engine-managed and cannot be made selectable. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.UNION_NO_EFFECT` | Union has no visible effect. | The shapes already overlap or are identical. Pick distinct shapes to see a union result. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.UNKNOWN` | Block \{block} is unknown. | The block id no longer references a live design block. Re-resolve the id, or guard with isValid(block) before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.UNKNOWN_BLOCK_TYPE` | Unknown block type: \{type} | Block type '\{type}' is not registered. Use a built-in type or register a custom type before calling create(). | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.UNKNOWN_FILL_TYPE` | Unknown fill type: \{type} | Fill type '\{type}' is not registered. Supported types: color, gradient/linear, gradient/radial, gradient/conical, image, video, pixelStream. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.UNKNOWN_SHAPE_TYPE` | Unknown shape type: \{type} | Shape type '\{type}' is not registered. Use a built-in shape type or register a custom one before creation. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.UUID_UNSUPPORTED` | The block does not have a UUID. | Only renderable design blocks carry a UUID. Check api.block.getType(block) is a design block before calling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.VALUES_NOT_FINITE_THREE` | The values \{first}, \{second}, and \{third} must be finite numbers. | All three values must be finite. Replace any NaN or infinity with a concrete number. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.VALUES_NOT_FINITE_TWO` | The values \{first} and \{second} must be a finite number. | Both values must be finite. Replace any NaN or infinity with a concrete number. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.VALUE_NOT_FINITE` | The value \{value} must be a finite number. | Pass a finite numeric value. NaN and ±infinity are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.VALUE_NOT_FINITE_IN_UNIT_RANGE` | The value \{value} must be a finite number in the range \[0, 1]. | Clamp the value to \[0, 1] before passing it in. NaN and infinities are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.VALUE_NOT_NUMBER` | The value \{value} must be a number. | Pass a number. NaN is rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.VALUE_SET_NAME_INVALID` | "\{name}" is not a valid value set name. | A value set name must not be empty and must not start or end with whitespace. | | | `BLOCK.VALUE_SET_NAME_TAKEN` | A value set with name "\{name}" already exists. | Pick a name that no value set of the scene uses, or rename the value set '\{name}' first. | | | `BLOCK.VALUE_SET_NOT_FOUND` | The scene has no value set with id "\{id}". | The value set is destroyed or belongs to another scene. Get the ids of the scene with findAllValueSets() or findValueSetByName(). | | | `BLOCK.VARIABLE_BINDING_GRADIENT_STOPS_UNSUPPORTED` | Gradient color stops cannot be bound to a variable. | Bind a color variable to a solid color fill instead, or set the stops with setGradientColorStops. | | | `BLOCK.VARIABLE_BINDING_UNSUPPORTED` | A \{type} variable cannot be bound to property "\{property}". | Bind color variables to Color properties, number variables to Float properties, image variables to image and video source URIs, and typeface variables to text/typeface or caption/typeface. String variables cannot be bound. | | | `BLOCK.VARIABLE_NAME_INVALID` | "\{name}" is not a valid variable name. | A variable name must not be empty, must not start or end with whitespace, and must not contain double curly braces, because text templates use them to mark variables. | | | `BLOCK.VARIABLE_NAME_TAKEN` | A variable with name "\{name}" already exists. | Pick a name that no variable uses, or remove the variable '\{name}' first. | | | `BLOCK.VARIABLE_NOT_FOUND` | Variable with name "\{name}" not found. | Variable '\{name}' has not been declared. Call setVariable(name, value) once before reading. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.VARIABLE_TYPEFACE_INVALID` | Typeface variable "\{name}" needs a family name and at least one font. | Pass a typeface with a non-empty name and a non-empty fonts list, for example one from the typeface asset source. | | | `BLOCK.VARIABLE_TYPE_MISMATCH` | Variable "\{name}" is a \{type} variable, not a \{expected} variable. | Use the getter and setter for type '\{type}', or remove the variable with removeVariable(name) first. | | | `BLOCK.VIDEO_FILL_NO_URI` | The video fill block does not have a valid file URI. | Set a non-empty fileURI on the video fill via setURI() before invoking this API. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.VIDEO_LOAD_FAILED` | Failed to load video. | The video source could not be decoded or fetched. Verify the URI is reachable, the container/codec is supported, and the bytes are a valid video. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.VIDEO_RESOURCE_NOT_LOADED` | The video resource has not been loaded yet. | Wait for the video resource to reach Ready, or call forceLoadAVResource(block, callback) before retrying. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.VIDEO_RESOURCE_NOT_LOADED_FOR_OPERATION` | The video resource has not been loaded yet. Please ensure the video is loaded before \{operation}. | Await the resource-loaded signal (e.g. the \`'audio'\`/\`'video'\` block-state transition to \`Ready\`) before invoking \`\{operation}\`. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `BLOCK.WIDTH_INVALID_FOR_SCALING` | Current width is invalid for scaling. | The block's current width is zero or non-finite. Set a valid width before scaling. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | ## CODEC Audio/video codec capability and decoding. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `CODEC.ANDROID_AUDIO_ENCODER_CREATE_FAILED` | Cannot create audio encoder: media\_status = \{mediaStatus} | Android MediaCodec returned media\_status=\{mediaStatus}. Consult the AMediaCodec status codes. | | | `CODEC.ANDROID_JNI_ERROR` | \{reason} | The Android MediaCodec/JNI layer reported: \{reason} | | | `CODEC.APPLE_OSSTATUS_FAILURE` | \{operation} failed (\{status}) | The Apple media-framework call '\{operation}' returned a non-success status: \{status}. The codec, container, or hardware session could not be configured. | | | `CODEC.AUDIO_DECODER_CREATE_FAILED` | Could not create audio decoder: \{reason} | The platform's audio decoder rejected the configuration. Reason: \{reason} | | | `CODEC.AUDIO_DECODER_FATAL` | Encountered fatal audio decoder error: \{reason} | The audio decoder produced an unrecoverable error during playback. Reason: \{reason} | | | `CODEC.AUDIO_DECODER_METADATA_INVALID` | Invalid audio metadata. | The audio metadata required to construct the decoder is missing or invalid. | | | `CODEC.AUDIO_DECODER_NO_CHUNKS` | Audio has no chunks. | The audio resource has no decoded chunks available. Ensure the source has audible data. | | | `CODEC.AUDIO_DECODER_NO_FRAMES` | Audio has no frames. | The audio resource has no decoded frames. The source may be empty or unparseable. | | | `CODEC.AUDIO_DECODER_ZERO_FRAMES_PER_CHUNK` | Frames per chunk is zero. | Audio chunking metadata is invalid; cannot decode. Re-encode the source. | | | `CODEC.AUDIO_DECODER_ZERO_FRAMES_PER_PACKET` | Frames per packet is zero. | Audio packetization metadata is invalid; cannot construct the decoder. Re-encode the source. | | | `CODEC.AUDIO_DECODE_UNSUPPORTED` | Audio decoding is not supported on this platform. | This build does not include an audio decoder. Rebuild with the relevant codec backend, or run on a supported platform. | | | `CODEC.AUDIO_ENCODER_CONFIG_INVALID` | Invalid audio encoder configuration: \{channels} channels at \{sampleRate} Hz | Audio channels and sample rate must be positive. Got channels=\{channels}, sampleRate=\{sampleRate}. | | | `CODEC.AUDIO_ENCODER_CREATE_FAILED` | Could not create audio encoder: \{reason} | The platform's audio encoder rejected the configuration. Reason: \{reason} | | | `CODEC.AUDIO_ENCODER_FATAL` | Encountered fatal audio encoder error: \{reason} | The audio encoder stopped during the export. Export again. If it fails again in a browser, use a different browser. | | | `CODEC.AUDIO_ENCODE_UNSUPPORTED` | Audio encoding is not supported on this platform. | This build does not include an audio encoder. Rebuild with the relevant codec backend, or run on a supported platform. | | | `CODEC.AUDIO_TRACK_NOT_FOUND` | Couldn't find audio track in AVContainer. | The container has no audio track. Use a media file with an audio stream. | | | `CODEC.BACKEND_TEXTURE_INCOMPLETE` | Backend texture is incomplete. | The Skia backend texture is missing state required for codec read-back. | | | `CODEC.EMPTY_AUDIO_CODEC_STRING` | Empty audio codec string. | The container did not declare an audio codec. Re-encode the file with explicit codec metadata. | | | `CODEC.EMPTY_VIDEO_CODEC_STRING` | Empty video codec string. | The container did not declare a video codec. Re-encode the file with explicit codec metadata. | | | `CODEC.ENCODER_STATE_NOT_FOUND` | Could not find encoder state. | The encoder state was released. Construct a fresh encoder before calling this API. | | | `CODEC.GSTREAMER_CREATE_ELEMENT_FAILED` | Could not create GStreamer element \{name} of type \{factory} | GStreamer factory '\{factory}' is not installed. Install the corresponding gst plugin or pick a different element. | | | `CODEC.GSTREAMER_CREATE_SINK_CAPS_FAILED` | Could not create sink caps. | GStreamer rejected the sink caps. The target codec or format may not be supported. | | | `CODEC.GSTREAMER_CREATE_SOURCE_CAPS_FAILED` | Could not create source caps. | GStreamer rejected the source caps. The codec or media format may not be supported. | | | `CODEC.GSTREAMER_LINK_AUDIO_ENCODER_FAILED` | Could not link audio encoder pipeline elements. | GStreamer could not connect the audio encoder branch. Verify supported caps. | | | `CODEC.GSTREAMER_LINK_AUDIO_FAILED` | Could not link audio pipeline elements. | GStreamer could not connect audio elements. Caps negotiation failed. | | | `CODEC.GSTREAMER_LINK_VIDEO_ENCODER_FAILED` | Could not link video encoder pipeline elements. | GStreamer could not connect the video encoder branch. Verify supported caps. | | | `CODEC.GSTREAMER_LINK_VIDEO_FAILED` | Could not link video pipeline elements. | GStreamer could not connect video elements. Caps negotiation failed. | | | `CODEC.GSTREAMER_PIPELINE_CREATE_FAILED` | Could not create a new GStreamer pipeline. | gst\_pipeline\_new() failed. The GStreamer runtime may be in a bad state. | | | `CODEC.GSTREAMER_PIPELINE_ERROR` | \{reason} | GStreamer pipeline reported: \{reason} | | | `CODEC.GSTREAMER_UNEXPECTED_MESSAGE` | Unexpected message error type. | The GStreamer bus reported an unexpected message kind. This may indicate a pipeline state mismatch. | | | `CODEC.METAL_TEXTURE_FROM_IOSURFACE_FAILED` | Failed to create Metal texture from IOSurface. | Metal rejected the IOSurface-backed texture descriptor. The pixel format or surface dimensions may be unsupported. | | | `CODEC.NO_CONTEXT` | No context. | The compute context is not initialized. | | | `CODEC.NO_GPU_CONTEXT` | No GPU context. | The compute context lacks GPU support required for this codec. | | | `CODEC.OFFSCREEN_CANVAS_CREATE_FAILED` | Could not create offscreen canvas. | Offscreen canvas allocation failed. Out-of-memory or unsupported size is the likely cause. | | | `CODEC.OFFSCREEN_CONTEXT_UNAVAILABLE` | Cannot get offscreen context for encoding. | The compute context is not an offscreen (Metal) context. Video encoding requires an offscreen GPU context. | | | `CODEC.PIXEL_BUFFER_NO_IOSURFACE` | CVPixelBuffer has no IOSurface backing. | The pooled pixel buffer is not IOSurface-backed, so it cannot be wrapped as a Metal texture. The pool was created without the IOSurface property. | | | `CODEC.PIXEL_BUFFER_POOL_CREATE_FAILED` | Failed to create CVPixelBufferPool. | CoreVideo could not allocate a pixel buffer pool for the encoder. Out-of-memory or an unsupported pixel format is the likely cause. | | | `CODEC.PRESENTATION_TIMESTAMPS_NOT_UNIQUE` | Presentation timestamps are not unique. | The video track has duplicate presentation timestamps, which prevents building a frame index. Re-mux the source with monotonically increasing PTS. | | | `CODEC.RECORDING_CONTEXT_UNAVAILABLE` | Could not obtain a recording context. | The Skia display canvas has no GPU recording context. The compute context may not be initialized for hardware video decoding. | | | `CODEC.UNKNOWN_AUDIO_DECODER_HANDLE` | Unknown audio decoder handle. | The audio decoder handle is not registered. It may have been destroyed already. | | | `CODEC.UNKNOWN_CODEC_STRING` | Unknown codec string \{codec} | The codec identifier '\{codec}' is not recognized by this build. Pass a supported codec string (e.g. an H.264 video or AAC audio identifier) when configuring the export. See the supported codecs list for valid identifiers. | [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/import-media/file-format-support-8cdc84/) | | `CODEC.UNKNOWN_VIDEO_DECODER_HANDLE` | Unknown video decoder handle \{handle} | The decoder handle is not registered. It may have been destroyed already. | | | `CODEC.UNKNOWN_VIDEO_DECODER_HANDLE_NO_ARG` | Unknown video decoder handle. | The decoder handle is not registered. It may have been destroyed already. | | | `CODEC.UNREACHABLE_FOR_CODEC` | Unreachable code reached for codec \{codec} | Internal switch did not handle codec '\{codec}'. This is an engine bug — please file a ticket. | | | `CODEC.UNSUPPORTED_CODEC_STRING` | Unsupported codec string \{codec} | Codec '\{codec}' is recognized but not supported in the current configuration. Choose a widely supported codec such as H.264 for video or AAC for audio. See the supported codecs list for the full set. | [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/import-media/file-format-support-8cdc84/) | | `CODEC.UNSUPPORTED_H265_CODEC_STRING` | Unsupported H265 codec string \{codec} | The H.265 profile encoded in '\{codec}' is not supported by GStreamer in this build. See the supported codecs list for alternatives. | [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/import-media/file-format-support-8cdc84/) | | `CODEC.VIDEO_BITRATE_INVALID` | Video bitrate must be equal or greater than 0. | Pass a non-negative \`bitrate\` value to the video encoder configuration. | | | `CODEC.VIDEO_DECODER_CREATE_FAILED` | Could not create video decoder: \{reason} | The platform's video decoder rejected the configuration. Reason: \{reason} | | | `CODEC.VIDEO_DECODER_FATAL` | Encountered fatal video decoder error: \{reason} | The decoder produced an unrecoverable error during playback. Reason: \{reason} | | | `CODEC.VIDEO_DECODER_UNRESPONSIVE` | Video decoder has been unresponsive for more than 10 seconds. | Possible decoder deadlock or hardware stall. Reset the decoder or fall back to a software path. | | | `CODEC.VIDEO_DECODE_UNSUPPORTED` | Video decoding is not supported on this platform. | This build does not include a video decoder. Rebuild with the relevant codec backend, or run on a supported platform. | | | `CODEC.VIDEO_ENCODER_BUSY` | Already encoding another video. | Wait for the current video encoding session to finish before starting another. | | | `CODEC.VIDEO_ENCODER_CREATE_FAILED` | Could not create video encoder: \{reason} | The platform's video encoder rejected the configuration. Reason: \{reason} | | | `CODEC.VIDEO_ENCODER_FATAL` | Encountered fatal video encoder error: \{reason} | The video encoder stopped during the export. Lower the resolution and export again. | | | `CODEC.VIDEO_ENCODER_INVALID_RESOLUTION` | Invalid resolution for video encoder: \{width} x \{height} | Both dimensions must be > 0 and within the encoder's frame size limit. On Linux without the licensed codec pack, and on Windows, a frame has at most 36864 macroblocks of 16x16 pixels, such as 4096x2304. On Windows each side is at least 48 pixels. Got \{width}x\{height}. | | | `CODEC.VIDEO_ENCODE_UNSUPPORTED` | Video encoding is not supported on this platform. | This build does not include a video encoder. Rebuild with the relevant codec backend, or run on a supported platform. | | | `CODEC.VIDEO_SESSION_CREATE_FAILED` | VTCompressionSessionCreate failed. | VideoToolbox could not create a compression session for the requested codec and resolution. The codec may be unsupported on this hardware. | | | `CODEC.VIDEO_TRACK_NOT_FOUND` | Couldn't find video track in AVContainer. | The container has no video track. Use a media file with a video stream. | | | `CODEC.WEBCODECS_INVALID_FORMAT` | Invalid codec string format for WebCodecs: \{codec} | WebCodecs requires a fully-qualified codec id. '\{codec}' is not in the expected format. | | | `CODEC.WEBCODECS_NOT_AVAILABLE_NODE` | WebCodecs API is not available in Node.js. | Run in a browser environment, or use a different codec backend on Node. | | | `CODEC.WEBCODECS_NOT_SUPPORTED` | WebCodecs API is not supported. | The current browser does not expose the WebCodecs API. Upgrade to a supported version. | | ## COLOR ICC color profiles, document CMYK profiles, and color management. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `COLOR.DOCUMENT_PROFILE_NOT_SET` | The document has no profile in the requested slot. | Set a profile in this slot before reading its information. | | | `COLOR.IMAGE_DECODE_FAILED` | The CMYK image data could not be decoded. Size: \{byteCount} bytes. | The CMYK or YCCK samples of the image could not be read. The image loads without color management instead, so its colors can be wrong. | | | `COLOR.IMAGE_TRANSFORM_FAILED` | The color profile of the image at \{uri} could not be applied. | The profile could be read, but the engine could not convert colors with it. The image loads without color management, so its colors can be wrong. | | | `COLOR.IMPORTED_IMAGE_COLOR_SPACE_INVALID` | The imported image color space value \{value} is not valid. | Use DeviceGray, DeviceRGB, DeviceCMYK or ICCBased. | | | `COLOR.IMPORTED_IMAGE_COLOR_TRANSFORM_INVALID` | The imported image color transform value \{value} is invalid. | Use 0 or 1, or omit the color transform. | | | `COLOR.IMPORTED_IMAGE_DECODE_INVALID` | The imported image decode ranges are invalid. | Provide two finite numbers per image component, or omit decode for identity. | | | `COLOR.IMPORTED_IMAGE_DEFINITION_NOT_FOUND` | The image fill has no active imported image definition. | Attach a definition to the fill's current image before reading it. | | | `COLOR.IMPORTED_IMAGE_HAS_SOURCE_SET` | The image fill uses a source set. | Remove the source set before attaching a definition of imageFileURI. | | | `COLOR.IMPORTED_IMAGE_MASK_INVALID` | The imported image mask is invalid: \{reason} | Use packed 1-bit stencil samples with byte-aligned rows and decode \[0, 1] or \[1, 0], or a grayscale JPEG. Match positive pixel dimensions, at most 20000 per side and 67108864 pixels total. Limit bytes to 128 MiB. Soft masks require JPEG bytes. Optional matte components must be finite and match the image color space. Do not combine nonempty colorKeyMask with a stencil mask. Soft masks discard colorKeyMask. | | | `COLOR.IMPORTED_IMAGE_NO_IMAGE` | The image fill has no image to describe. | Set fill/image/imageFileURI before attaching its imported image definition. | | | `COLOR.IMPORTED_IMAGE_PROFILE_MISMATCH` | The imported image profile does not match its color space. | Provide profile bytes only for ICCBased, and declare ICCBased only when the effective space is ICCBased. | | | `COLOR.IMPORTED_IMAGE_RECORD_INVALID` | The imported image record is invalid. | Keep the record below 64 KiB and name its format when it is present. | | | `COLOR.PROFILE_ASSIGNMENT_SUPERSEDED` | The color profile at \{uri} was not assigned, because a later change replaced the request. | Another profile assignment, a profile removal or a scene load happened while this profile was loading. The later change is in effect. Set the profile again if you still want it. | | | `COLOR.PROFILE_DATA_SPACE_MISMATCH` | The color profile data describes \{space}, but \{expected} is required here. | Pass the bytes of a profile whose color space matches the one it is used for. The profile must match the slot’s component space. The previous profile stays in use. | | | `COLOR.PROFILE_INVALID` | The ICC profile data could not be read. Size: \{byteCount} bytes. | The bytes are not a valid ICC profile. Check that the file is complete and was not truncated by the download. | | | `COLOR.PROFILE_MISSING` | The color profile at \{uri} could not be loaded. | Check that the URI is reachable and returns valid ICC profile data. A setter keeps the previous profile. A profile named by the scene or the setting is skipped: colors of that space use the next profile the engine can load, and are previewed unmanaged when there is none. | | | `COLOR.PROFILE_NOT_LOADED` | The CMYK color profile is not loaded yet, so the color cannot be converted. | The call started the load. Run \`update()\` and convert again. Converting without the profile would return a different color than the engine renders. | | | `COLOR.PROFILE_SLOT_INVALID` | The document color profile slot is not valid. | Use a Default or Output role with an RGB, CMYK or Gray component space. | | | `COLOR.PROFILE_SPACE_MISMATCH` | The color profile at \{uri} describes \{space}, but \{expected} is required here. | Point the profile at a file whose color space matches the one it is used for. A document profile must match its slot. The \`fallbackCMYKProfileUri\` setting must name a CMYK profile. A setter keeps the previous profile. A profile named by the scene or the setting is skipped, and colors use the next profile. | | | `COLOR.PROFILE_UNSUPPORTED_SPACE` | The ICC profile uses the color space \{space}, which is not supported. | Use an RGB, CMYK or Gray profile. Other color spaces cannot be opened. | | | `COLOR.RENDERING_INTENT_INVALID` | The rendering intent value \{value} is not valid. | Use 0 (Perceptual), 1 (RelativeColorimetric), 2 (Saturation) or 3 (AbsoluteColorimetric). The previous rendering intent stays in use. | | ## COMPUTE Compute contexts (Metal, GL, CPU) and capability negotiation. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `COMPUTE.COLOR_SPACE_BIT_DEPTH_UNSUPPORTED` | The current device does not support the required pixel bit depth for the requested color space. | Wide-gamut color spaces require 10+ bpp surfaces. Use sRGB on devices that don't expose deep-color buffers. | | | `COMPUTE.COLOR_SPACE_DISPLAY_UNSUPPORTED` | The current device does not support displaying the requested color space. | The device's display pipeline cannot render this color space. Fall back to sRGB or query supported spaces first. | | | `COMPUTE.COLOR_SPACE_UNSUPPORTED_BY_ENGINE` | The engine does not support the requested color space on this platform. | Pick a supported color space (sRGB / Display P3) for this build, or rebuild the engine with the appropriate Skia configuration. | | | `COMPUTE.CONTEXT_CREATE_FN_NOT_FOUND` | Context creation function not found for type \{type}. | \`createContext()\` was called with a context type the build was not compiled with. Rebuild with the appropriate \`HAS\_UBQ\_\*\_CONTEXT\` macro defined, or pass a supported type. | | | `COMPUTE.D3D12_CONTEXT_CREATE_FAILED` | Could not create a Direct3D 12 render context: \{reason} | The machine has no Direct3D 12 runtime, or no GPU that runs it at feature level 11\_0. Render on the CPU instead: \`device: 'auto'\` does that by itself. | | | `COMPUTE.DATA_PROVIDER_EMPTY` | \{kind} data provider is empty. | No bytes have been registered for this resource yet. Ensure the data was provided before reading. | | | `COMPUTE.DATA_PROVIDER_NOT_CONTIGUOUS` | Data provider does not expose contiguous data. | The active data provider serves data in chunks. Use the streaming API instead of asking for a contiguous buffer. | | | `COMPUTE.DATA_PROVIDER_NOT_FULLY_AVAILABLE` | \{kind} data provider is not fully available. | The provider is still streaming. Wait for the resource state to transition to Ready before requesting the full buffer. | | | `COMPUTE.DATA_PROVIDER_TOO_LARGE` | \{kind} data provider too large for contiguous data. | The resource exceeds the contiguous-buffer threshold. Read in chunks via the streaming API instead. | | | `COMPUTE.EGL_CREATE_CONTEXT_FAILED` | EGL create context error. | \`eglCreateContext\` returned \`EGL\_NO\_CONTEXT\`. The requested GL version / attributes are not supported by the driver. | | | `COMPUTE.EGL_CREATE_SURFACE_FAILED` | EGL create surface error. | \`eglCreateWindowSurface\` / \`eglCreatePbufferSurface\` returned \`EGL\_NO\_SURFACE\`. The native window or pbuffer attributes are incompatible with the chosen EGL config. | | | `COMPUTE.EGL_INVALID_CONTEXT_TYPE` | Invalid context type passed to the creation function. | The EGL context factory received an unexpected type tag. Confirm the caller selects one of the supported context types. | | | `COMPUTE.EGL_NO_CONFIGS_MATCH` | No EGL configs match the requested attribute list. | The requested EGL config attributes (color depth, surface type, GL profile) are not satisfiable on this device. Relax the attribute list or fall back to a CPU context. | | | `COMPUTE.EGL_NO_DISPLAY` | EGL display unavailable. | \`eglGetDisplay\` returned \`EGL\_NO\_DISPLAY\`. The platform does not expose a usable EGL display — fall back to a CPU context. | | | `COMPUTE.EGL_OPERATION_FAILED` | EGL \{operation} error: \{reason} | The EGL driver reported a failure during \`\{operation}\`. Inspect \`reason\` for the underlying EGL error string and ensure the platform's EGL stack is initialized correctly. | | | `COMPUTE.EMSCRIPTEN_MAKE_CONTEXT_CURRENT_FAILED` | Could not make the WebGL context current. | \`emscripten\_webgl\_make\_context\_current\` failed. The browser may have lost the context or the canvas was detached from the DOM. | | | `COMPUTE.GR_DIRECT_CONTEXT_CREATE_FAILED` | Creating a Skia GrDirectContext failed. | Skia could not initialize a direct GPU context from the current EGL/GL surface. Inspect Skia logs for the underlying cause; falling back to a CPU context is the safe option. | | | `COMPUTE.HTTP_DATA_NO_BUFFER` | HTTP data provider has no buffer. | The HTTP data provider has not yet allocated its receive buffer. Wait for the first chunk before reading. | | | `COMPUTE.IMAGE_DECODE_FAILED` | Failed to decode the image. | The image data is not a supported raster format, is corrupt, or the texture for it could not be created. | | | `COMPUTE.MP3_PARSE_TRACK_DATA_FAILED` | Failed to parse MP3 file: could not load track data. | The MP3 parser could not extract track frames. The file may be truncated or have an unsupported container variant. | | | `COMPUTE.MP3_PARSE_TRACK_METADATA_INVALID` | Failed to parse MP3 file: invalid track metadata (channels, sample rate, or frames). | The MP3 header lists impossible values for channels / sample rate / frames. Re-encode the file. | | | `COMPUTE.MP4_DURATION_ZERO` | MP4 duration is zero. | The container reports zero duration. The file may be truncated or be a fragmented MP4 missing the duration box. | | | `COMPUTE.MP4_TIMESCALE_ZERO` | MP4 timescale is zero. | The container reports zero timescale. The header is malformed; regenerate the file. | | | `COMPUTE.OPFS_READ_FAILED` | Failed to read OPFS data. | The Origin Private File System rejected the read. The file may have been removed or the browser revoked access. | | | `COMPUTE.SKIA_GL_INTERFACE_INVALID` | Skia OpenGL interface failed validation. | The GL function pointers Skia loaded for this context do not satisfy its requirements. Verify the GL driver advertises the extensions Skia depends on. | | | `COMPUTE.VIDEO_UNSUPPORTED_AUDIO_TRACKS` | Video has \{count} unsupported audio track\{plural}. | Tracks with unsupported codecs were removed. Re-encode with AAC or another supported codec to retain audio. | | ## EDITOR Editor state, history, selection, commands. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `EDITOR.ASSET_DROP_FAILED` | \{reason} | The default \`asset.drop\` action could not apply the dropped asset. \`reason\` names the step that failed, such as a scene that a load replaced while the asset was fetched. | | | `EDITOR.AUDIO_BUFFER_INVALID_SIZE` | Invalid audio buffer: size \{bufferSize} is not a multiple of frame size \{frameSize}. | Audio buffer length must be aligned to the frame size. Trim the buffer to a multiple of \{frameSize} bytes before submitting. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AUDIO_BUFFER_INVALID_URI` | Invalid buffer URI: \{uri} | The audio buffer URI '\{uri}' does not resolve to a registered buffer. Confirm the buffer was created and the URI matches. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AUDIO_BUFFER_NO_DATA` | No buffer data available. | The audio buffer block has no data registered for its file URI. Ensure data is written to the buffer before requesting chunks. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AUDIO_DECODER_CREATE_FAILED` | Could not create audio decoder: \{reason} | The platform's audio decoder rejected the input. Confirm the codec is supported on this platform. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AUDIO_DECODE_FAILED` | Fatal audio decoding error: \{reason} | The audio decoder produced an unrecoverable error during playback. The source may be corrupted or use an unsupported format. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AUDIO_FETCH_FAILED` | Failed to fetch audio. | The audio resource could not be retrieved. Check the URI, asset-source registrations, and network connectivity. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AUDIO_INVALID_RANGE` | Invalid range requested: start = \{start}, end = \{end}. | Audio chunk ranges must satisfy 0 \<= start \< end. Adjust the request bounds and retry. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AUDIO_NOT_LOADED` | The audio has not been loaded yet. | Call forceLoadAVResource(block, callback) or wait for the resource state to reach Ready before querying audio metadata. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AUDIO_UNSUPPORTED_FORMAT` | Unsupported audio format with channels: \{channels}, bits: \{bits}, sample format: \{sampleFormat}. | The engine cannot resample this combination on the current platform. Re-encode the audio to a supported profile. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AV_BLOCK_INVALID_WITH_HINT` | The block with ID \{id} is not valid. It may have been deleted, e.g., when a new scene was loaded. | Re-resolve the block id (\{id}) before calling AV APIs. Async callbacks frequently outlive their target block. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AV_DURATION_UNDEFINED` | The AV container duration is undefined. | The decoder could not determine the resource's total duration. The container may be malformed or use an unsupported codec. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AV_OPERATION_AUDIO_OR_VIDEO_FILL` | This operation is only supported for audio blocks and video fills. | Pass an audio block, or resolve the video fill of the block via getFill(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AV_OPERATION_BLOCK_TYPE_UNSUPPORTED` | This operation is only supported for video fills and audio blocks. | Resolve the video fill of the block via getFill(block), or pass an audio block directly. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.AV_OPERATION_VIDEO_ONLY` | This operation is only supported for video fills. | Only blocks whose fill is a video fill expose video-specific metadata. Audio blocks don't apply here. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.BLOCK_NOT_ATTACHED_TO_PAGE` | The target block is not attached to the page. | Add the block as a child of the page before setting it as the page's duration source. | [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) | | `EDITOR.BLOCK_NOT_A_PAGE` | The given page block is not a page. | The first argument must be a page block. Use api.block.findByType('//ly.img.ubq/page') to obtain one. | [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) | | `EDITOR.BLOCK_NO_DURATION` | The target block has no duration. | The block has no Duration component. Confirm the block type supports durations, or check via supportsDuration(block) first. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.BLOCK_NO_DURATION_AS_PAGE_SOURCE` | The target block doesn't support duration and therefore cannot be duration source. | Pick a block that has a Duration component (e.g. video fill, audio, or track) to drive the page's duration. | [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) | | `EDITOR.BLOCK_NO_DURATION_SUPPORT` | The target block doesn't support durations. | Duration applies only to time-aware blocks (pages, tracks, video/audio fills). Use supportsDuration(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.BLOCK_NO_PLAYBACK_CONTROL_SUPPORT` | The target block doesn't support playback control. | Playback control (looping, muted, speed, volume) applies only to AV-source blocks. Use supportsPlaybackControl(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.BLOCK_NO_PLAYBACK_SUPPORT` | The target block doesn't support playback. | Playback time applies only to blocks with a PlaybackTime component (scenes, pages, video/audio). Use supportsPlaybackTime(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.BLOCK_NO_TIME_OFFSET_SUPPORT` | The target block doesn't support time offsets. | Time offsets apply only to time-aware children of a track. Use supportsTimeOffset(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.BLOCK_NO_TIME_REMAP_SUPPORT` | The target block doesn't support time remapping. | Time remapping applies only to group blocks. Set it on a group and set its 'group/composition' property to true. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.BLOCK_NO_TRIM_SUPPORT` | The target block doesn't support trimming. | Trimming applies only to video fills, audio blocks, and similar AV-source blocks. Use supportsTrim(block) before calling. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CAMERA_CLAMP_ACROSS_SCENES` | Cannot clamp camera to elements of different scenes. | All blocks passed to camera clamping must belong to the same scene. Group by scene before calling. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CAMERA_CLAMP_BLOCKS_WITHOUT_SCENE` | Cannot clamp camera to blocks without a scene. | Attach the blocks to a scene before enabling camera clamping. Floating blocks are not eligible. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CAMERA_CLAMP_EMPTY_BLOCKS` | Cannot clamp camera to an empty block list. | Provide at least one block (or the scene itself) to clamp against. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CAMERA_CLAMP_NO_PAGES` | No pages found in the scene. | Camera clamping with page-carousel features requires at least one page block. Add a page before enabling clamping. | [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) | | `EDITOR.CAMERA_CLAMP_PAGE_NOT_LAYOUTED` | The page has not been layouted. | Layout has not run yet for this page. Trigger an engine update or wait one frame before enabling camera clamping. | [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) | | `EDITOR.CAMERA_ENTITY_INVALID` | Camera entity is invalid. | Could not resolve a valid camera for the provided scene or camera id. Confirm the scene has a main camera and the id references it. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CAMERA_NOT_VALID` | Camera is not valid. | The active scene has no camera entity. Create the scene through the normal \`scene.create\*()\` path so the camera is set up automatically. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `EDITOR.CAMERA_POSITION_CLAMP_NOT_ENABLED` | This block does not have camera position clamping enabled. | Enable camera position clamping for this block first, or check whether it is enabled before disabling it. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CAMERA_ZOOM_CLAMP_LIMITS_INVALID` | To enable camera zoom clamping, at least one of minZoomLimit or maxZoomLimit must be positive. | Set at least one of minZoomLimit or maxZoomLimit to a positive value. Use a negative value on the other side to leave it unbounded. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CAMERA_ZOOM_CLAMP_LIMITS_ORDER` | The minZoomLimit has to be smaller or equal to maxZoomLimit. | Swap or adjust the values so minZoomLimit \<= maxZoomLimit. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CAMERA_ZOOM_CLAMP_NOT_ENABLED` | This block does not have camera zoom clamping enabled. | Enable camera zoom clamping for this block first, or check whether it is enabled before disabling it. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CAMERA_ZOOM_LIMITS_NOT_FINITE` | The zoom limits must be finite numbers. | Both minZoomLimit and maxZoomLimit must be finite. Use a negative value to indicate no limit on that side. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CLIP_DEFAULT_DURATION_INVALID` | The default clip duration must be finite and greater than zero. | Set clip/defaultDurationValue to a positive number of seconds. | [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) | | `EDITOR.COMMAND_ARG_TYPE_MISMATCH` | Argument at index \{index} for command '\{command}' doesn't match expected type '\{expectedType}'. Got '\{actualType}' instead. | Convert the argument to '\{expectedType}' or pass a different value matching the command signature. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.COMMAND_NOT_REGISTERED` | \{command} is not a registered command. | Command '\{command}' was not found in the registry. Confirm the name is correct and that the responsible module is loaded. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.COMMAND_RETURN_TYPE_MISMATCH` | Returned value for command '\{command}' did not match expected type (got '\{actualType}', expected '\{expectedType}'). Did the responsible system never execute? | The system handling '\{command}' returned a value of the wrong type. Verify the handler is registered and producing '\{expectedType}'. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.COMMAND_UNIMPLEMENTED` | Unimplemented. | The command type has no implementation of this method. Subclasses must override determineType() and any other defaults they rely on. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.COMMAND_WRONG_ARG_COUNT` | Received \{received} arguments for command \{command}, but expected \{expected}. | Command '\{command}' takes \{expected} arguments. Adjust the call to match the documented signature. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CROP_ELEMENT_NOT_CROPPABLE` | This element can't be cropped. | Crop operations require a block with a cropped fill (image or video). Verify api.block.supportsCrop(id) before calling \{operation}. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CROP_NO_SELECTED_ELEMENT` | Select an element to crop. | Select a block before invoking \{operation}. Use api.block.findAllSelected() to verify a selection exists. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.CURRENT_PAGES_DUPLICATE_PAGE` | The list of current pages holds the page \{block} more than once. | Pass each page once. | [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) | | `EDITOR.CURRENT_PAGES_INVALID_COUNT` | The list of current pages must hold at least one page. | Pass one page block, or more distinct page blocks. Two pages form a facing-page view. | [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) | | `EDITOR.FONT_DATA_LOAD_FAILED` | Font resource ready but failed to load font data: \{uri}. | The font resource finished loading but the data could not be parsed. The payload may be empty or use an unsupported font format. | [Custom Fonts](#broken-link-9565b3) | | `EDITOR.FONT_LOAD_FAILED` | Failed to load font '\{uri}': \{reason}. | The font resource at '\{uri}' could not be loaded (\{reason}). Verify the URI is reachable and the asset is registered. | [Custom Fonts](#broken-link-9565b3) | | `EDITOR.FONT_METRICS_EXTRACT_FAILED` | Failed to extract font metrics from: \{uri}. | The font file at '\{uri}' could not be parsed for metrics. Confirm the file is a valid TTF/OTF and not corrupted. | [Custom Fonts](#broken-link-9565b3) | | `EDITOR.FONT_URI_EMPTY` | Font file URI cannot be empty. | Pass a non-empty URI pointing to a font file registered with the engine. See addLocalAssetSourceFromJSON or registerFont docs for setup. | [Custom Fonts](#broken-link-9565b3) | | `EDITOR.GLOBAL_SCOPE_INVALID_VALUE` | The global scope \{scope} holds an invalid value. | The stored value of '\{scope}' is not Allow, Deny or Defer, so the settings of a loaded scene may be corrupt. Call setGlobalScope() to set it again. | [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) | | `EDITOR.HISTORY_HANDLE_INVALID` | Could not obtain a valid history handle. | The editor has no active history buffer. Confirm a scene is loaded and the engine has fully initialized before invoking history APIs. | [Undo And History](https://img.ly/docs/cesdk/mac-catalyst/concepts/undo-and-history-99479d/) | | `EDITOR.MEMORY_QUERY_UNAVAILABLE` | Could not obtain \{kind} memory for the current platform. | The platform does not expose \{kind}-memory statistics, or the query failed. Treat memory metrics as best-effort on this platform. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.MOVEMENT_CONSTRAINT_NEGATIVE` | setMovementConstraint value must be non-negative; call removeMovementConstraint to clear a constraint. | Pass a non-negative distance, or call removeMovementConstraint(targets) instead of using a negative sentinel. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.NEGATIVE_DURATION` | Negative durations are not supported. | Pass a non-negative duration. Use 0 for an empty interval or std::numeric\_limits\::infinity() for unbounded. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.NO_SCENE_AVAILABLE` | No scene available. | The editor has no active scene. Load or create one before invoking this API. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.NO_UNDO_STEP_AVAILABLE` | No undo step available to remove. | The undo stack is empty. Verify the editor has performed at least one undoable action before calling removeUndoStep(). | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.PADDING_NOT_FINITE` | The padding values must be finite numbers. | All four padding components must be finite. Replace any NaN or infinity with a concrete number (zero is fine). | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.PAGES_NOT_RESIZED` | Pages could not be resized. | Resize failed for one or more pages. Confirm each page is unlocked (api.block.isTransformLocked / isAllowedByScope) and that the requested width and height are finite and positive. | [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) | | `EDITOR.PAGE_CONTENT_ASPECT_RATIO_INVALID` | ContentAspectRatio preset cannot be used to create a new page; it only applies to blocks with intrinsic content dimensions. | Pages have no intrinsic content size. Use a FreeAspectRatio, FixedAspectRatio, or FixedSize preset instead. | | | `EDITOR.PAGE_RESIZE_DISABLED` | Page resizing interaction is disabled in UBQ settings. | Enable page resizing via settings (e.g. 'features/page/resize') before invoking the resize interaction. | [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) | | `EDITOR.PAGE_RESIZE_FIXED_ASPECT_ONLY` | Page resizing interaction is restricted to fixed aspect ratio in UBQ settings. | Allow free-aspect resizing in settings, or constrain the interaction to the configured fixed aspect ratio. | [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) | | `EDITOR.PLAYBACK_SPEED_OUT_OF_RANGE` | Invalid playback speed \{speed}. Must be between 0.25 and 3.0. | For non-video blocks playback speed must be in \[0.25, 3.0]. Video fills support a wider range. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.PLAYBACK_SPEED_TOO_LOW` | Invalid playback speed \{speed}. Must be at least 0.25. | Pass a playback speed of at least 0.25. Speeds below this are not supported. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.RESOURCE_LOAD_FAILED` | Failed to load resource \{uri}. | The resource at '\{uri}' could not be loaded. Check that the URI is reachable, CORS allows access, and the format is supported. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.RESOURCE_URI_EMPTY` | The resource has an empty URI. | Set a non-empty file URI on the video fill or audio block before requesting a load. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.ROLE_NOT_FOUND` | Role \{role} not found. | '\{role}' is not a known editor role. Valid roles are defined by the EditorRole enum (e.g. Adopter, Creator). | [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) | | `EDITOR.SAFE_AREA_INSETS_NEGATIVE` | Safe area inset values must be non-negative. | Each inset describes a non-negative distance from the viewport edge. Use 0 to disable an inset rather than a negative value. | | | `EDITOR.SAFE_AREA_INSETS_NOT_FINITE` | Safe area inset values must be finite numbers. | All four inset components must be finite. Replace any NaN or infinity with a concrete number (zero is fine). | | | `EDITOR.SCENE_CONTENT_EMPTY` | Received empty scene content. | The scene-serialization command was called with an empty payload. Pass the serialized scene string returned by \`scene.saveToString(...)\`. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `EDITOR.SCENE_DECOMPRESS_FAILED` | Failed to decompress scene data. | The compressed scene payload could not be decoded. The file may be truncated or produced by an incompatible compressor. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.SCENE_ENTITY_INVALID` | Scene entity is invalid. | Could not resolve a valid scene for the provided id. Confirm the id is a scene or a camera attached to a scene. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.SCENE_INPUT_INVALID` | Received scene input is not a valid serialization. | The payload does not parse as a CE.SDK scene document, or its blocks do not form one tree under a scene: two blocks share an id, or the hierarchy names a missing block, a parent loop or a child twice. Confirm it was produced by api.scene.saveToString() or saveToArchive(). | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.SCENE_MISSING_REQUIRED_KEY` | Invalid scene file: "\{key}" key not found. | The serialized scene is missing the required top-level "\{key}" field. The file may be truncated or produced by an incompatible writer. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.SCENE_REQUIRES_URL_LOAD` | Scene is part of an archive and must be loaded via URL. | Use api.scene.loadFromURL() or loadFromArchive() rather than loadFromString() for archive-bound scenes. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.SCENE_SIZE_UNAVAILABLE` | Could not get scene size. Scene may not have a layout yet. | Wait for the first layout pass to complete (e.g. after the next \`api.update()\`) before reading scene dimensions. | | | `EDITOR.SCENE_UNKNOWN_BLOCK_TYPE` | Unknown block type in scene: \{type} | The scene has a block of type '\{type}', which this engine version does not know. A newer engine may have saved the scene. Upgrade the engine, or remove the block with the engine that saved the scene. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.SETTING_ENUM_VALUE_NOT_FOUND` | Enum value \{value} not found. | '\{value}' is not a member of the target enum. Call getSettingEnumOptions() to list valid values. | [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) | | `EDITOR.SETTING_NOT_ENUM` | Setting \{keypath} is not an enum. | Use the type-specific getter/setter (Bool/Int/Float/String/Color) instead of the Enum variant for non-enum settings. | [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) | | `EDITOR.SETTING_NOT_FOUND` | Setting \{keypath} not found. | No setting registered at '\{keypath}'. Use findAllSettings() to discover valid key paths. | [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) | | `EDITOR.SETTING_TYPE_MISMATCH` | Setting \{keypath} has type \{kind}. | Call getSetting\{kind}() or setSetting\{kind}() for '\{keypath}'. getSettingType() returns the type of a setting. | [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) | | `EDITOR.SETTING_TYPE_UNSUPPORTED` | Unsupported setting type for \{keypath}. | The setting at '\{keypath}' has a type the public API does not expose. Use getSettingType() to discover the supported categories. | [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) | | `EDITOR.SPLIT_BLOCK_FAILED` | Could not split block of type "\{blockType}": \{reason} | Splitting a block failed at an intermediate step (\{reason}). Confirm the block via api.block.isValid(block) and api.block.supportsDuration(block), and that the split time lies inside the block's time range (api.block.getTimeOffset(block) to getTimeOffset + getDuration). | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.THUMBNAIL_SAMPLES_INVALID` | Can't generate thumbnail sequence with samplesPerChunk \<= 0. | Pass a positive samplesPerChunk so each chunk contains a non-empty waveform sample window. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.TOUCH_ROTATE_TOO_MANY_POINTS` | Touch rotate event has more than two touch points. | The rotate gesture is only defined for two-finger input. Cancel the gesture when a third pointer joins. | | | `EDITOR.TRIM_OFFSET_UNDEFINED` | The trim offset is undefined. | The AV resource duration or playback speed yields a non-finite trim offset. Confirm the resource is loaded and playback speed is positive. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.UNIT_CONVERSION_FAILED` | Failed to convert to given design unit. | The requested unit conversion is not supported in this context. Confirm the scene has a configured DPI and design unit. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.UNSUPPORTED_SERIALIZATION_FORMAT` | Unsupported serialization format. | The scene declares a serialization format this engine version cannot read. Re-save the scene with the current engine, or upgrade the engine. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.VALUE_NOT_FINITE` | The value \{value} must be a finite number. | NaN and infinity are not accepted. Pass a finite floating-point value. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.VECTOR_INVALID_MIRROR_MODE` | Invalid mirror mode \{mode}. Expected 0 (None), 1 (AngleAndLength), or 2 (AngleOnly). | Pass 0, 1, or 2 — values map to the VectorHandleMirrorMode enum (None, AngleAndLength, AngleOnly). | [Vector Edit](#broken-link-f3a7b2) | | `EDITOR.VECTOR_NODE_NOT_DELETABLE` | Cannot delete node: contour must have at least 3 nodes. | A vector contour cannot be reduced below 3 nodes. Delete the entire contour or merge it with another path instead. | [Vector Edit](#broken-link-f3a7b2) | | `EDITOR.VECTOR_NO_CONTROL_POINT_SELECTED` | No vector control point is selected. | Vector control-point operations require an active selection. Select a control point first before invoking this API. | [Vector Edit](#broken-link-f3a7b2) | | `EDITOR.VECTOR_NO_NODE_SELECTED` | No vector node is selected. | Vector node operations require an active selection. Select a node first via the vector editor UI or programmatically before invoking this API. | [Vector Edit](#broken-link-f3a7b2) | | `EDITOR.VECTOR_NO_PATH_EDITING` | No vector path is being edited. | Enter vector edit mode on a vector block before invoking this API. Vector path mutations require an active edited path. | [Vector Edit](#broken-link-f3a7b2) | | `EDITOR.VIDEO_DECODER_CREATE_FAILED` | Could not create video decoder: \{reason} | The platform's video decoder rejected the input. Confirm the codec is supported on this platform — see CE.SDK capability docs. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.VIDEO_DECODE_FAILED` | Fatal video decoding error: \{reason} | The decoder produced an unrecoverable error during playback. The source may be corrupted or use an unsupported codec profile. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.VIDEO_FETCH_FAILED` | Failed to fetch video. | The video resource could not be retrieved. Check the URI, asset-source registrations, and network connectivity. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.VIDEO_NOT_LOADED` | The video has not been loaded yet. | Call forceLoadAVResource(block, callback) or wait for the resource state to reach Ready before querying video metadata. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.ZOOM_AUTO_FIT_NOT_ENABLED` | This block does not have zoom auto-fit enabled. | Call enableZoomAutoFit(block) before disabling, or check isZoomAutoFitEnabled(block) first. | [Editing Workflow](https://img.ly/docs/cesdk/mac-catalyst/concepts/editing-workflow-032d27/) | | `EDITOR.ZOOM_BLOCK_NOT_LAYOUTED` | Could not zoom to block. Block hasn't been layouted yet. | Wait for the first layout pass to complete (e.g. after the next \`api.update()\`) before zooming to the block. | | | `EDITOR.ZOOM_NO_CAMERA` | No valid camera exists, can't zoom to block. | Ensure the scene has been initialized with a camera before invoking zoom-to-block. | | ## ENCODE Image and video encoding/export. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `ENCODE.AUDIO_BLOCK_DIRECT_EXTRACTION_UNSUPPORTED` | Unable to extract MP4 audio directly from audio block. The audio file may not be in a compatible format or may not be loaded. | Re-encode the audio block as MP4-compatible AAC before attempting direct extraction, or load the block first. | | | `ENCODE.AUDIO_BLOCK_INVALID` | Invalid audio block entity. | The id does not reference a live audio block. Re-resolve it before exporting and verify with api.block.isValid(block). | | | `ENCODE.AUDIO_BUFFER_EMPTY` | Audio buffer is empty. | The audio block has zero bytes of audio data. Provide a valid, non-empty audio source for the block before exporting. | | | `ENCODE.AUDIO_BUFFER_NOT_FOUND` | Audio buffer not found. | The audio block's buffer is not registered. Ensure audio data was provided before export. | | | `ENCODE.AUDIO_CHANNEL_COUNT_INVALID` | Number of audio channels must be \{expected}. | Audio export requires \{expected} channels. Convert mono/multichannel sources first. | | | `ENCODE.AUDIO_CHUNK_READ_FAILED` | Failed to read audio chunk data from data provider. | The data provider returned no bytes for an audio chunk. The source may be truncated. | | | `ENCODE.AUDIO_CODEC_CONTAINER_MISMATCH` | A \{container} file cannot hold \{codec} audio. | Use AAC or Opus for mp4, and Opus or Vorbis for webm. Auto picks a codec that fits. | | | `ENCODE.AUDIO_CODEC_INVALID` | Unknown audio codec \{value}. | Pass AudioCodec.Auto, AudioCodec.AAC, AudioCodec.Opus or AudioCodec.Vorbis as the audio codec. | | | `ENCODE.AUDIO_CODEC_UNSUPPORTED` | This platform cannot encode \{codec} audio. | Leave the audio codec on Auto, so that the export picks a codec this platform can encode. Check the \{capability} capability before you ask for \{codec}. | | | `ENCODE.AUDIO_CONCURRENT_ENCODING` | An audio export that mixes a page cannot start inside the work of another one, such as from its progress callback. | Start the next audio export after the running one delivers its result, also after a cancel. | | | `ENCODE.AUDIO_CONTEXT_NOT_AVAILABLE_DURING_EXPORT` | Audio context not available during export. | The audio context was destroyed mid-export. Keep it alive until the export callback fires. | | | `ENCODE.AUDIO_CONTEXT_PARAMS_INVALID` | Invalid audio context parameters. | Pass a supported sample rate and channel count in the audio export options before exporting. | | | `ENCODE.AUDIO_CONTEXT_UNAVAILABLE` | No audio context available for export. | Audio context is not initialized. Create one with the active compute context before exporting. | | | `ENCODE.AUDIO_DURATION_INVALID` | Duration must be a positive finite number. | Audio export duration must be > 0 and finite. Check the source has a well-defined duration. | | | `ENCODE.AUDIO_EXPORT_OPTIONS_NOT_OBJECT` | Audio export options must be a valid JSON object. | Pass the audio export options as a JSON object string, for example one that sets "sampleRate" to 48000. | | | `ENCODE.AUDIO_EXPORT_OPTIONS_PARSE_FAILED` | Failed to parse audio export options: \{reason} | The audio export options string is not valid JSON (\{reason}). Fix the JSON syntax and retry. | | | `ENCODE.AUDIO_FRAME_CALC_INVALID` | Invalid audio frame calculations. | Computed frame counts are nonsensical (zero or negative). Verify duration, sample rate, and processing rate. | | | `ENCODE.AUDIO_FRAME_SIZE_CALC_INVALID` | Invalid audio frame size calculations. | Frame-size math produced zero or negative values. Verify channel count and bit depth. | | | `ENCODE.AUDIO_INVALID_SAMPLE_RATE_FOR_TIMESTAMP` | Invalid sample rate for timestamp conversion. | Source audio sample rate is zero or negative. Verify it before computing timestamps. | | | `ENCODE.AUDIO_MIME_TYPE_INVALID` | Mime type must be "audio/wav", "audio/mp4" or "audio/webm". | Pass audio/wav, audio/mp4 or audio/webm as the target mime type for audio export. | | | `ENCODE.AUDIO_MP4_NO_DATA_MUXED` | No audio data was muxed into MP4 container. | The MP4 file received no audio data. For example, Chrome and Edge on Windows before version 157 encode nothing from less than about 0.05 seconds of audio. Export at least 0.05 seconds of audio. | | | `ENCODE.AUDIO_MUXER_DESTROYED` | Muxer was destroyed before finalization. | The MP4 muxer was released early. Keep the encode service alive until the result callback fires. | | | `ENCODE.AUDIO_NO_CHUNKS_IN_RANGE` | No audio chunks found in the specified time range. | The selected time range has no audio data. Adjust start/end times or verify the audio source covers the requested span. | | | `ENCODE.AUDIO_NO_DATA_CAPTURED` | No audio data captured during export. | The export pipeline produced no audio samples. Verify the source has audible content within the requested range. | | | `ENCODE.AUDIO_NO_PCM_COLLECTED` | No PCM data was collected. | The decoder returned zero PCM samples. The audio source may be empty or fully silent. | | | `ENCODE.AUDIO_PROCESSING_RATE_INVALID` | Audio processing rate must be a positive finite number. | Pass a positive, finite audio sample rate in the audio export options before exporting. | | | `ENCODE.AUDIO_SAMPLE_RATE_INVALID` | Sample rate must be \{expected}. | Audio export requires sample rate \{expected}. Resample the source before exporting. | | | `ENCODE.AUDIO_SERVICE_UNAVAILABLE` | No audio service available for export. | The engine was built without audio support. Enable it in the build config and rebuild. | | | `ENCODE.AUDIO_SINGLE_TRACK_INDEX_NONZERO` | Single-track audio container only supports trackIndex=0, got \{index} | For single-track containers, pass trackIndex=0. | | | `ENCODE.AUDIO_STAGING_WRITE_FAILED` | Failed to write the audio export to temporary storage. | The audio could not be written to a temporary file, so the export was stopped instead of returning an incomplete file. Check that the temporary directory exists and has free space, then retry. | | | `ENCODE.AUDIO_START_TIME_OUT_OF_BOUNDS` | Start time \{start} is out of bounds. Valid range: 0-\{max} | Audio start time \{start} must be within \[0, \{max}]. | | | `ENCODE.AUDIO_TIME_RANGE_INVALID` | Invalid time range: start=\{start}, end=\{end} | Audio time range must satisfy 0 \<= start \< end. Got start=\{start}, end=\{end}. | | | `ENCODE.AUDIO_TIME_RANGE_INVALID_AFTER_TRIM` | Invalid time range after applying trim settings and options. | After applying trim/clip parameters the selected range is empty. Adjust start/end times. | | | `ENCODE.AUDIO_TRACK_INDEX_OUT_OF_BOUNDS` | Audio track index \{index} is out of bounds. Valid range: 0-\{max} | Pass an audio track index within \[0, \{max}]. | | | `ENCODE.AUDIO_UNSUPPORTED_EXPORT_FORMAT` | Unsupported audio export format. | Use audio/wav or audio/mp4 as the export target. | | | `ENCODE.AUDIO_WAV_TOO_LONG` | A WAV file holds at most \{maxSeconds} seconds of audio. | Export the audio in parts with timeOffset and duration. | | | `ENCODE.BLOCKS_STAYED_PENDING` | The export stopped because blocks \{blocks} stayed pending for \{seconds} seconds without loading progress. | A resource of these blocks did not load, for example a buffer that holds no data or a download that stopped. Check the resources of these blocks, or raise the exportPendingTimeoutSeconds setting. | | | `ENCODE.BLOCK_MUST_BE_PAGE` | The block to export must be a page. | Video export targets a single page block. Pass a page or call api.scene.getPages() to obtain one. Scenes with multiple pages must be exported one page at a time. | | | `ENCODE.BLOCK_SIZE_ZERO` | Block to export has size 0. | Set a positive width and height on the block, or pick a different block, before exporting. | | | `ENCODE.BUFFER_COPY_ALLOC_FAILED` | Not enough memory to copy buffer \{bufferUri} for the export. | The export works on a copy of every buffer in the scene, so memory must hold each buffer twice. Destroy or shrink large buffers, then retry. | | | `ENCODE.CANCELLED_BY_BLOCK_ERROR` | The export was cancelled due to block \{block} having an error: \{reason} | Block \{block} entered an error state during export (\{reason}). Resolve the block's underlying issue and retry. | | | `ENCODE.COLOR_MASK_DATA_FAILED` | Color masking data could not be generated. | The renderer failed to produce the color-mask buffer. Retry, or simplify the scene if it has very high resolution. | | | `ENCODE.COLOR_MASK_DISABLED` | Color masking is not enabled. | Enable color masking via the export options before requesting mask data. | | | `ENCODE.CONTEXT_AUDIO_CREATE_FAILED` | Could not create audio context. | Audio context creation failed. Ensure the platform exposes an AudioContext and is not in a frozen tab. | | | `ENCODE.CONTEXT_RENDER_CREATE_FAILED` | Could not create render context. | GPU context creation failed. Confirm the host supports the required backend (WebGL2/Metal) and retry. | | | `ENCODE.DIRECT_EXTRACTION_UNSUPPORTED` | Direct extraction not supported for this block/mime type combination. | The requested block kind cannot be extracted directly to this mime type. Use a transcoding path instead. | | | `ENCODE.ENTITY_INVALID` | Entity is invalid. | The block id no longer references a live block. Re-resolve before exporting. | | | `ENCODE.ENTITY_NOT_PART_OF_PAGE` | Entity is not part of a valid page. | Audio export requires the block to be inside a page. Add the block to a page before exporting. | | | `ENCODE.EXPORT_CANCELLED` | The export was cancelled by the receiver. | The export was cancelled: its task, coroutine or abort signal was cancelled, or its chunk callback or progress callback returned false. No further data will be delivered. Start a new export if the result is still needed. | | | `ENCODE.EXPORT_FAILED` | Export failed. | A non-specific export error was produced. Check earlier log lines for the root cause and retry. | | | `ENCODE.EXPORT_OPTIONS_NOT_OBJECT` | Export options must be a valid JSON object. | Pass the export options as a JSON object string, for example one that sets "pngCompressionLevel" to 6. | | | `ENCODE.EXPORT_OPTIONS_PARSE_FAILED` | Failed to parse export options: \{reason} | The export options string is not valid JSON (\{reason}). Fix the JSON syntax and retry. | | | `ENCODE.EXPORT_TRANSACTION_BLOCK_INVALID` | An export transaction must export a scene, or a page of a scene, with at least one visible page. | Pass the scene or one of its pages to beginExportTransaction, and keep a page of it visible until the transaction ends. | | | `ENCODE.EXPORT_TRANSACTION_SIZE_INVALID` | The size and the duration of an export transaction must be 0 or more. | Pass 0 or more bytes and a finite duration of 0 or more seconds. Pass 0 when a value is not known. | | | `ENCODE.EXPORT_TRANSACTION_TYPE_INVALID` | The export transaction type "\{type}" is not valid. | Use 1 to 32 lowercase letters, digits and hyphens. The types image, video, audio and internal are reserved for the engine. | | | `ENCODE.EXPORT_TRANSACTION_UNKNOWN` | No open export transaction has the id "\{transactionId}". | Pass the id that beginExportTransaction returned, before the transaction ends. | | | `ENCODE.GPU_LOST_GENERIC` | GPU context was lost during export rendering. Try exporting at a lower resolution. | Reduce the export resolution or split the scene, then retry. Restart the engine if the GPU context fails to recover. | | | `ENCODE.GPU_LOST_PDF` | GPU context was lost during PDF export rendering. The scene may contain elements that exceed the device's capabilities. | Lower the export DPI or split the scene into multiple pages, then retry. | | | `ENCODE.GPU_LOST_SVG` | GPU context was lost during SVG export rendering. Try reducing the size of large images. | Re-encode large source images to a lower resolution before exporting, or restart the engine to recover the GPU context. | | | `ENCODE.GROUP_INVALID` | Invalid group entity. | The group id does not reference a live group. Re-resolve it before exporting and verify with api.block.isValid(group). | | | `ENCODE.GROUP_NOT_IN_PAGE_HIERARCHY` | Group entity not found in page hierarchy. | The group is not a descendant of the page passed to the encode call. | | | `ENCODE.GROUP_NOT_PART_OF_PAGE` | Group is not part of a valid page. | Groups must live under a page. Reparent the group or pick a page descendant. | | | `ENCODE.GROUP_NO_CHILDREN` | Group has no child blocks. | Audio export requires at least one block in the group. | | | `ENCODE.IMAGE_JPEG_FAILED` | Failed to encode JPEG. | The JPEG encoder rejected the input. | | | `ENCODE.IMAGE_JPEG_OOM` | Failed to encode JPEG. Memory allocation failed. | Not enough memory for the JPEG buffer. | | | `ENCODE.IMAGE_PNG_FAILED` | Failed to encode PNG. | The PNG encoder rejected the input. The image dimensions or pixel format may be unsupported. | | | `ENCODE.IMAGE_PNG_OOM` | Failed to encode PNG. Memory allocation failed. | Not enough memory for the PNG buffer. Reduce dimensions or free memory. | | | `ENCODE.IMAGE_READ_PIXELS_FAILED` | Failed to read pixels. | The source image could not be read into a CPU buffer. The compute context may be missing GPU read-back support. | | | `ENCODE.IMAGE_TGA_OOM` | Failed to encode TGA. Memory allocation failed. | Not enough memory for the TGA buffer. Reduce dimensions or free memory. | | | `ENCODE.IMAGE_TGA_REQUIRES_RGBA32` | TGA encoding only supports 32-bit RGBA pixel data. | Convert the image to 32-bit RGBA before encoding to TGA. | | | `ENCODE.IMAGE_UNKNOWN_MIME` | Unknown MIME type for image encoding: \{mimeType} | Mime type '\{mimeType}' is not supported. Use one of image/png, image/jpeg, image/webp, image/tga. | | | `ENCODE.IMAGE_WEBP_FAILED` | Failed to encode WebP. | The WebP encoder rejected the input. | | | `ENCODE.IMAGE_WEBP_OOM` | Failed to encode WebP. Memory allocation failed. | Not enough memory for the WebP buffer. | | | `ENCODE.INSUFFICIENT_RESOURCES` | Not enough resources available on device. Try exporting at a lower resolution. | Reduce the requested export resolution or simplify the scene to fit available memory. | | | `ENCODE.MASK_BUFFER_FAILED` | Could not acquire mask buffer. Please try again. | The renderer transiently failed to allocate a mask buffer. Retry the export after a short delay. | | | `ENCODE.MASK_COLOR_OUT_OF_RANGE` | Mask color values must be between 0 and 1. | Clamp each mask color component (r/g/b/a) to \[0, 1] before passing to the export options. | | | `ENCODE.MIME_TYPE_INVALID` | Invalid mime-type '\{mimeType}', expected '\{a}', '\{b}' or '\{c}'. | Use one of '\{a}', '\{b}', or '\{c}'. Update the asset's mime type at registration time. | | | `ENCODE.NOT_ALL_RESOURCES_LOADED` | Not all resources were loaded. | A file failed to load, or an image file cannot be decoded. Read the state of each block to find it. Use a file that loads and decodes, then load again. | | | `ENCODE.OFFSCREEN_CANVAS_CREATE_FAILED` | Could not create offscreen canvas. | Offscreen canvas allocation failed. Confirm the host environment supports the required GPU surface. | | | `ENCODE.OUTPUT_SIZE_EXCEEDS_MAX` | The output size of \{width}x\{height} px for the export exceeds the maximum supported size \{maxSize} of the device. | Reduce the export resolution so its larger dimension is at most \{maxSize} px. | | | `ENCODE.OUTPUT_SIZE_INSUFFICIENT_RESOURCES` | Could not export at \{width}x\{height}. Not enough resources available on device. Try exporting at a lower resolution. | Reduce the export resolution from \{width}x\{height} or simplify the scene, then retry. | | | `ENCODE.PAGE_NO_CHILDREN` | Page has no children. | Audio export requires at least one child block on the page. | | | `ENCODE.PDF_COLOR_MASK_UNSUPPORTED` | Color masking is not supported for PDF export. | PDF output cannot carry a color mask. Disable color masking or export as PNG, JPEG or WebP instead. | | | `ENCODE.PDF_CREATE_FAILED` | Failed to create a PDF document. | The PDF writer rejected the document. Check available memory and retry with a smaller scene if needed. | | | `ENCODE.PDF_CREATE_FAILED_RESOURCES` | Failed to create PDF document. Not enough resources available on device. Try exporting at a lower resolution. | Reduce the page size or DPI and retry. Very large scenes can exceed device memory limits. | | | `ENCODE.PDF_RENDER_SIZE_EXCEEDS_MAX` | An image, gradient or effect of the PDF export renders at \{width}x\{height} px (at \{dpi} DPI), which exceeds the maximum supported size \{maxSize} of the device. Try reducing the scene DPI or the size of that element. | Reduce the scene DPI, the export size, or the size of the largest image, gradient or effect, so it renders at or below \{maxSize} px. | | | `ENCODE.PDF_STAGING_WRITE_FAILED` | Failed to write the PDF export to temporary storage. | The document could not be staged on disk, so the export was stopped instead of returning a truncated file. Free up storage space and retry. | | | `ENCODE.PIXEL_BUFFER_UNEXPECTED_SIZE` | Unexpected pixel buffer size for color analysis. | The pixel buffer dimensions don't match the expected analysis layout. Re-read the buffer or verify the source surface. | | | `ENCODE.PIXEL_STREAM_NO_DATA` | Failed to encode pixel stream: no data was produced. | Inspect the pixel stream pipeline. The exporter produced an empty result for the requested options. | | | `ENCODE.RELATIVE_URLS_NOT_SUPPORTED` | Relative URLs are not supported. | Resolve all relative URLs to absolute ones before invoking the operation. | | | `ENCODE.RESOURCE_DATA_EMPTY` | Empty resource data: \{uri} | The resource at '\{uri}' loaded but returned empty bytes. Re-source the file. | | | `ENCODE.RESOURCE_LOAD_FAILED_WITH_REASON` | Failed to load resource '\{uri}': \{reason} | The resource at '\{uri}' failed to load (\{reason}). Confirm reachability and supported format. | | | `ENCODE.RESULT_BUFFER_FAILED` | Could not acquire result buffer. Please try again. | The renderer transiently failed to allocate a result buffer. Retry the export after a short delay. | | | `ENCODE.STREAM_EXPORT_MIME_TYPE_INVALID` | Streamed export only supports "application/pdf". | Pass application/pdf as the mime type or use exportToBuffer for other formats. | | | `ENCODE.STREAM_EXPORT_MISSING_RECEIVER` | Streamed export was started without a data receiver. | Pass a chunk callback to the streamed export. There is nothing to deliver the data to. | | | `ENCODE.SVG_CANVAS_CREATE_FAILED` | Failed to create SVG canvas. | The SVG canvas could not be allocated. Check available memory and retry. | | | `ENCODE.SVG_COLOR_MASK_UNSUPPORTED` | Color masking is not supported for SVG export. | SVG output cannot carry a color mask. Disable color masking or export as PNG, JPEG or WebP instead. | | | `ENCODE.SVG_MEMORY_ALLOC_FAILED` | Could not allocate memory for SVG export. | Reduce the scene complexity or available pixel count, then retry the SVG export. | | | `ENCODE.SVG_NO_DATA` | SVG export produced no data. | The renderer produced an empty SVG. Confirm the page has visible content and try again. | | | `ENCODE.TARGET_NOT_IN_PAGE_HIERARCHY` | Target entity not found in page hierarchy. The entity may not be part of this page. | Verify the target block is a descendant of the page passed to the encode call. | | | `ENCODE.TRACK_INVALID` | Invalid track entity. | The track id does not reference a live track. Re-resolve it before exporting and verify with api.block.isValid(track). | | | `ENCODE.TRACK_NOT_IN_PAGE_HIERARCHY` | Track entity not found in page hierarchy. | The track is not a descendant of the page passed to the encode call. | | | `ENCODE.TRACK_NOT_PART_OF_PAGE` | Track is not part of a valid page. | Tracks must live under a page. Reparent the track or pick a page descendant. | | | `ENCODE.TRACK_NO_CHILDREN` | Track has no child blocks. | Audio export requires at least one block on the track. | | | `ENCODE.VIDEO_ALPHA_UNSUPPORTED` | Only VP8 and VP9 video in a video/webm file keeps transparency, and the web encodes none. | Set exportAlpha only for a video/webm export with VP8 or VP9 on Android, iOS, macOS, Linux or Windows. | | | `ENCODE.VIDEO_BLOCK_HAS_ERROR` | The export was cancelled due to block \{block} having an error: \{reason} | Block \{block} reported an error during export: \{reason}. Fix the block before retrying. | | | `ENCODE.VIDEO_CODEC_CONTAINER_MISMATCH` | A \{container} file cannot hold \{codec} video. | Use H.264 or HEVC for video/mp4, and VP8, VP9 or AV1 for video/webm. Auto picks a codec that fits. | | | `ENCODE.VIDEO_CODEC_INVALID` | Unknown video codec \{value}. | Pass VideoCodec.Auto, H264, HEVC, VP8, VP9 or AV1 as the video codec. | | | `ENCODE.VIDEO_CONCURRENT_ENCODING` | The VideoEncodeService is currently encoding. Concurrent encoding is not supported. | Wait for the current video export to finish before starting another. | | | `ENCODE.VIDEO_COVER_METADATA_FAILED` | Failed to store the video cover image. | The MP4 metadata writer could not store the PNG cover image. Free memory and retry. | | | `ENCODE.VIDEO_COVER_TIME_INVALID` | Cover time must be a finite offset within the exported video duration. | Pass a coverTimeOffset greater than or equal to zero and less than the exported duration, or omit it. | | | `ENCODE.VIDEO_EXPORT_OPTIONS_NOT_OBJECT` | Video export options must be a valid JSON object. | Pass the video export options as a JSON object string, for example one that sets "videoBitrate" to 8000000. | | | `ENCODE.VIDEO_EXPORT_OPTIONS_PARSE_FAILED` | Failed to parse video export options: \{reason} | The video export options string is not valid JSON (\{reason}). Fix the JSON syntax and retry. | | | `ENCODE.VIDEO_FILL_AUDIO_REQUIRES_MP4` | Video fill audio extraction is only supported for audio/mp4 format. | Pass audio/mp4 as the target mime type when extracting audio from a video fill. | | | `ENCODE.VIDEO_FILL_INVALID` | Invalid video fill entity. | The video fill id does not reference a live fill. Re-resolve it with api.block.getFill(block) and confirm api.block.isValid(fill) before exporting. | | | `ENCODE.VIDEO_FILL_NO_URI` | The video fill block does not have a valid file URI. | Assign a valid fileURI to the video fill before requesting audio extraction. | | | `ENCODE.VIDEO_FRAME_FAILED` | Failed to encode video frame: \{reason} | The video encoder rejected a frame (\{reason}). Confirm codec compatibility and available memory, then retry. | | | `ENCODE.VIDEO_FRAME_RATE_INVALID` | Invalid video frame rate \{value}. It must be greater than 0 and at most \{max}. | Pass a framerate in (0, \{max}] frames per second, such as 24, 30 or 60, or omit it to use the scene's frame rate. | | | `ENCODE.VIDEO_HAS_NO_AUDIO_TRACKS` | The video does not contain any audio tracks. | Use a video that has at least one audio track for audio extraction. | | | `ENCODE.VIDEO_MIME_NOT_MP4` | Mime type is not "video/mp4" or "video/webm". | Video export supports video/mp4 and video/webm as the target mime type. | | | `ENCODE.VIDEO_NO_AUDIO_DURATION` | Video has no audio duration. | The video's audio track reports zero duration. The file may be malformed. | | | `ENCODE.VIDEO_PCM_READ_FAILED` | Could not read PCM frames. | The audio pipeline returned no PCM frames during video export. Audio context may have died. | | | `ENCODE.VIDEO_RESOURCE_LOAD_FAILED` | Failed to load video resource. Please ensure the video file is accessible and valid. | The video fill's URI could not be loaded. Check accessibility and format. | | | `ENCODE.VIDEO_STAGING_WRITE_FAILED` | Failed to write the video export to temporary storage. | The video could not be written to a temporary file, so the export was stopped instead of returning an incomplete file. Check that the temporary directory exists and is writable, then retry. | | ## EVENT Engine event subscription and dispatch. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `EVENT.SUBSCRIPTION_NOT_FOUND` | Event subscription does not exist. | The subscription id does not refer to an active subscription. It may have been unsubscribed already or never created. | [Events](https://img.ly/docs/cesdk/mac-catalyst/concepts/events-353f97/) | ## FETCH HTTP fetch and remote resource loading. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `FETCH.JSON_FETCH_FAILED` | Could not fetch JSON: \{uri} | The JSON resource at '\{uri}' could not be fetched. Check reachability, CORS, and the response status. | | | `FETCH.JSON_URI_EMPTY` | Could not fetch JSON: URI cannot be empty. | Pass a non-empty URI to the JSON fetch call. | | | `FETCH.RESOURCE_DATA_EMPTY` | Empty resource data: \{uri} | The resource at '\{uri}' loaded but returned no bytes. Re-source the file or confirm the URL serves content. | | | `FETCH.RESOURCE_FAILED` | Error fetching resource: \{url} | The HTTP fetch to '\{url}' failed. Inspect network status, response code, and CORS configuration. | | | `FETCH.URI_INVALID` | Invalid URI: \{uri} | The URI '\{uri}' is malformed or uses an unsupported scheme. Provide a fully qualified URL or a registered scheme. | | | `FETCH.URL_PARSE_FAILED` | Failed to parse URL: \{url} | The string '\{url}' is not a valid URL. Provide a fully qualified URL including the scheme. | | ## LICENSE License unlock, API-key handling, entitlement checks. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `LICENSE.ALREADY_UNLOCKED` | License is already unlocked. | The license has already been unlocked successfully. Skip the redundant unlock call. | | | `LICENSE.API_SERVICE_UNAVAILABLE` | API service is unavailable. Please contact support. | The license API endpoint did not respond. Check network connectivity; if the problem persists, contact support. | | | `LICENSE.AV_CONCURRENCY_LIMIT_REACHED` | Codec license concurrency limit reached: \{activeCount} of \{concurrencyLimit} licensed instances are already in use. | Wait for another engine instance to finish its video or audio work and retry, or raise the concurrency limit of your license. | | | `LICENSE.AV_SESSION_ACQUISITION_FAILED` | Could not reserve a video codec license from the license server. | Check network connectivity to the IMG.LY license service and retry the operation. | | | `LICENSE.AV_SESSION_REQUIRES_API_KEY` | Video codec licensing requires unlocking the engine with an API key. | Initialize the engine with the API key from your dashboard instead of an offline license file. | | | `LICENSE.CANNOT_DEACTIVATE_OFFLINE` | Cannot deactivate offline license. | Offline licenses cannot be deactivated remotely. Switch to an online license if dynamic activation is required. | | | `LICENSE.CLAIM_INVALID` | The license has a missing or invalid "\{claim}" value. | The license server issued a license that this SDK version cannot read. Contact support@img.ly and include this message. | | | `LICENSE.DEACTIVATION_TIMEOUT` | Deactivation timed out. | The license server did not acknowledge the deactivation in time. Retry, or contact support if the issue persists. | | | `LICENSE.ENGINE_VERSION_INVALID` | The License Key (API Key) you are using requires a newer version of the IMG.LY SDK. Please update to the latest version. | Upgrade the CE.SDK engine to a version compatible with this license. | | | `LICENSE.ENGINE_VERSION_OUT_OF_RANGE` | This license covers engine versions \{min} up to (but not including) \{max}; this build is \{current}. | Run an engine within the licensed range, or obtain a license that covers \{current}. An open bound is shown as '\*'. | | | `LICENSE.EXPIRED` | Thanks for using IMG.LY for creative editing. Please note that your license file or commercial use is expired. | Renew your subscription at https://img.ly/pricing to continue commercial use. | | | `LICENSE.IDENTIFIER_MISMATCH` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid for this app identifier. Current app identifier "\{current}" differs from license app identifiers: \[\{allowed}] | Your license is tied to specific app identifiers. Adjust the application bundle id to one of '\{allowed}' or update the license. | | | `LICENSE.INVALID` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid. | Verify the License Key matches your IMG.LY subscription. If issues persist, contact support@img.ly. | | | `LICENSE.INVALID_API_KEY` | Invalid API key. | The API key did not authenticate. Verify it matches the subscription registered in your IMG.LY dashboard. | | | `LICENSE.INVALID_FORMAT` | Invalid license format. Please contact support. | The license string could not be parsed. Verify it was copied in full and contact support if the issue persists. | | | `LICENSE.MISSING` | The license is missing. | Call api.unlockWithLicense() or unlockWithAPIKey() before invoking engine APIs that require entitlements. | | | `LICENSE.MISSING_FLUENDO_FLAG` | License does not support Fluendo codecs. | Add the 'fluendo' capability flag to your license, or do not request a session that requires it. | | | `LICENSE.MIXED_UNLOCK_METHODS` | unlockWithLicense shouldn't be called after unlockWithAPIKey. | Choose one unlock method per engine instance. Re-create the engine to switch between license-file and API-key authentication. | | | `LICENSE.NO_ACTIVE_TO_DEACTIVATE` | No active license to deactivate. | The deactivation request targets a license that is not currently active. Unlock a license first. | | | `LICENSE.NO_USER_ID` | License does not have a user ID. | User-specific entitlements require a license that carries a user id. Re-issue the license with a user id assigned. | | | `LICENSE.PLATFORM_MISMATCH` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid for this platform. Current platform: "\{current}" differs from license platforms: \[\{allowed}] | Your license restricts the platforms this engine may run on. Update the license to include '\{current}' or run on one of: \{allowed}. | | | `LICENSE.PRERELEASE_NOT_ALLOWED` | This license does not cover prerelease engine builds, and this build is \{current}. | Run a release build of the engine, or obtain a license whose config sets 'allowPrerelease' to true. | | | `LICENSE.PRODUCT_MISMATCH` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid for this product. Current build product: "\{current}" differs from license product: "\{required}" | Your license is tied to a specific product. Use a build for '\{required}' or obtain a license for '\{current}'. | | | `LICENSE.REQUEST_IN_PROGRESS` | License request already in progress. | A license fetch is in flight. Wait for it to finish before calling unlock again. | | | `LICENSE.SERVER_ERROR` | License server reported an error: \{reason} | Server response: \{reason}. If the message is unexpected, contact IMG.LY support. | | | `LICENSE.STILL_FETCHING` | License is still being fetched. | The license fetch has not completed. Await the unlock promise/callback before invoking entitlement-gated APIs. | | | `LICENSE.TARGET_MISMATCH` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid for this build target. Current build target triplet: "\{target}" | Your license restricts which build targets may run. Update the license to include '\{target}' or run on a permitted target. | | | `LICENSE.UNSUPPORTED_SESSION_TYPE` | Unsupported session type. | The requested session kind is not recognized. Pass a documented session type identifier. | | | `LICENSE.VERSION_INVALID` | The License Key (API Key) you are using to access the IMG.LY SDK is invalid for this version. | The license does not cover the running engine version. Update the license tier or downgrade the engine. | | ## MEDIA Media containers and demuxing. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `MEDIA.BLOCK_NOT_AUDIO_OR_VIDEO_FILL` | Entity is not an audio block nor a video fill. | Audio waveform/thumbnail APIs require an audio block or a block with a video fill. | | | `MEDIA.BLOCK_NOT_A_PAGE` | Block is not a page. | Page-grid thumbnail APIs require a page block. Pass a block where api.block.getType(id) == '//ly.img.ubq/page'. | | | `MEDIA.BLOCK_NOT_PAGE_OR_CHILD` | Block must be a page or a child of a page. | The block must live under a page in the scene tree. Reparent it or pick a page descendant. | | | `MEDIA.BLOCK_NOT_VALID` | Block is not valid. | The block id no longer references a live block. Re-resolve the id before requesting media. | | | `MEDIA.BLOCK_NOT_VIDEO_FILL` | Block is not a video fill. | This operation requires a block whose fill is a video. Verify api.block.getKind(id) returns 'video' before calling. | | | `MEDIA.BLOCK_SIZE_ZERO` | Block size is zero. | Cannot generate a thumbnail for a zero-sized block. Set the block's width and height first. | | | `MEDIA.CANVAS_SURFACE_GET_FAILED` | Could not get canvas surface. | The canvas does not expose its underlying surface. This indicates an internal renderer state issue. | | | `MEDIA.CHANNEL_COUNT_INVALID` | The number of channels must be 1 or 2. | Mono (1) and stereo (2) are the only supported channel counts. | | | `MEDIA.FRAME_COUNT_INVALID` | The number of frames must be greater than 0. | Pass a positive \`frameCount\`. | | | `MEDIA.GRID_DIMENSIONS_INVALID` | Rows and columns must be greater than 0. | Pass positive \`rows\` and \`columns\` for grid-thumbnail generation. | | | `MEDIA.IMAGE_ENCODE_FAILED` | Image encoding failed. | The encoder rejected the source image. This is usually an unsupported pixel format or zero-sized input. | | | `MEDIA.NEGATIVE_TIME_RANGE` | A negative time range is not allowed. | Ensure end > start (both in seconds, non-negative). | | | `MEDIA.OFFSCREEN_CANVAS_GET_FAILED` | Could not get offscreen canvas. | Skia surface has no canvas attached. The compute context may not have been initialized. | | | `MEDIA.OFFSCREEN_SURFACE_CREATE_FAILED` | Could not create offscreen surface. | Skia could not allocate the offscreen rendering surface. Out-of-memory or unsupported pixel format is the likely cause. | | | `MEDIA.OPERATION_UNSUPPORTED_FOR_BLOCK` | This operation is not supported for the given block. | The selected block type does not implement this media operation. Check api.block.getType(id) and call this API only on the block types it supports. | | | `MEDIA.SAMPLES_PER_CHUNK_INVALID` | The number of samples per chunk must be greater than 0. | Pass a positive \`samplesPerChunk\`. | | | `MEDIA.SAMPLE_COUNT_INVALID` | The number of samples must be greater than 0. | Pass a positive \`sampleCount\`. | | | `MEDIA.SNAPSHOT_FAILED` | Could not snapshot rendered thumbnail. | Skia could not produce an image snapshot from the rendered surface. The surface may not be readable on this backend. | | | `MEDIA.THUMBNAIL_ALLOC_FAILED` | Failed to allocate memory for thumbnail data. | The thumbnail size exceeds available memory. Reduce dimensions or free memory. | | | `MEDIA.THUMBNAIL_HEIGHT_INVALID` | The height of the thumbnail must be greater than 0. | Pass a positive \`height\` (pixels). | | | `MEDIA.THUMBNAIL_UPSCALE_FAILED` | Could not upscale thumbnail to requested size. | Upscaling failed mid-render. The destination size may be larger than the maximum surface size on this platform. | | | `MEDIA.UPSCALE_SURFACE_CREATE_FAILED` | Could not create upscale surface. | Skia could not allocate the destination surface for upscaling. Out-of-memory or unsupported config is the likely cause. | | | `MEDIA.VIDEO_FETCH_FAILED` | Failed to fetch video. | The video resource could not be retrieved. Check the URI, asset-source registrations, and network connectivity. | | ## SCENE Scene-level operations (load, save, archive, structural validation). | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `SCENE.ARCHIVAL_REQUEST_FAILED` | Archival request failed: \{reason} | An async archival operation (save/load) was completed in error state. The underlying reason is: \{reason} | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_ADD_RESOURCE_FAILED` | Could not add the resource '\{resource}' to the archive. Adding data failed. | The engine could not fetch or write the bytes for \{resource}. Verify the resource is reachable and not larger than the archive can hold. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_BUFFER_DATA_MISSING` | Could not add the resource '\{resource}' to the archive. Its buffer holds no data. | A buffer URL names data that only this engine holds, so the archive must carry the bytes. The buffer is empty or was destroyed. Write the data again with setBufferData, or clear the reference on the block, then save. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_CHUNK_READ_FAILED` | Failed to read chunk data from data provider. | The data provider returned no bytes for an available range. The underlying source may have disconnected or returned a partial response. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_CORRUPTED_EMPTY_RESOURCE` | Corrupted archive. Some elements in the scene are referencing empty data, e.g., '\{resource}'. | The archive is internally inconsistent. Regenerate it from the original scene and verify the source has no missing assets. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_CREATE_FAILED` | Could not create archive. | Final archive assembly failed for an unspecified reason. Inspect prior log lines for the underlying failure. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_DATA_PROVIDER_RANGE_UNAVAILABLE` | Data provider range is not available. | The requested byte range cannot be served by the underlying provider. Check the resource size and the requested offset/length. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_DATA_UNAVAILABLE_FOR_URL` | Archive data is not available for URL '\{url}'. | The archive references \{url} but the data could not be retrieved. Check the URL and the asset source it resolves through. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_ENTRY_STAGING_FAILED` | Could not write the archive entry '\{entry}' to temporary storage. | The engine keeps the files of a loaded archive in temporary files, and the disk could not hold '\{entry}'. Free disk space and load the archive again. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_FETCH_FAILED` | Failed to fetch archive from URL '\{url}': \{reason} | Network or asset-source failure. Verify the URL is reachable and the host serves the archive bytes. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_INVALID` | Not a valid archive. | The supplied bytes are not a recognizable CE.SDK archive. Was the file produced by saveToArchive()? | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_LOAD_AS_BLOCKS_NOT_SCENE` | Archive contains a blocks file. This archive has to be loaded as blocks and not as a scene. | Call loadFromArchiveAsBlocks() instead of loadFromArchive() for this file. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_LOAD_AS_SCENE_NOT_BLOCKS` | Archive contains a scene file. This archive has to be loaded as a scene and not as blocks. | Call loadFromArchive() instead of loadFromArchiveAsBlocks() for this file. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_MISSING_FILE` | Archive is missing a \{kind} file. | The archive is malformed: it does not contain the expected \{kind}.\* entry at its root. The file may be truncated or corrupted. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_NO_CURRENT_RESOURCE` | No current resource. | The data provider has not selected a resource. Call seekResource() (or the equivalent) before reading. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_NO_RESOURCE_BEGUN` | No resource begun or entry not open. | Call beginResource() before writing chunks or ending the resource. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_OFFSET_EXCEEDS_RESOURCE_SIZE` | Requested offset exceeds resource size. | Clamp the requested offset to be less than the resource's reported size before reading. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_RESOURCES_TOO_LARGE` | Resources too large. Not enough memory to create archive. | Allocation for the in-memory archive failed. Reduce scene size, free memory, or use a platform with more available RAM. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_RESOURCE_ALREADY_BEGUN` | Resource already begun. | A streaming resource is already in progress. Call endResource() before beginning another. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_RESOURCE_DATA_INVALID` | Resource data is invalid. | The bytes returned by the data provider failed validation. The archive may be corrupted. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_RESOURCE_DATA_UNAVAILABLE` | Resource data is not available. | The data provider has no bytes for the current resource. The underlying source may have been moved or deleted. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_STREAMED_WRITE_FAILED` | Failed to write streamed data to archive: \{reason} | Streaming a resource into the archive failed mid-flight. The underlying writer reported: \{reason} | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_WRITER_ALREADY_INITIALIZED` | Archive already initialized. | The ArchiveBufferWriter is already in an initialized state. Construct a new writer or release the current one before re-initializing. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ARCHIVE_WRITER_NOT_INITIALIZED` | Archive not initialized. | Call initialize() on the ArchiveBufferWriter before invoking entry or directory operations. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.BLOCKS_INPUT_INVALID` | Received blocks input is not a valid serialization. | The payload does not parse as a CE.SDK blocks document, or its hierarchy names a missing block, a parent loop or a child twice. Confirm it was produced by api.block.saveToString() or saveToArchive(). | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.BLOCKS_UNKNOWN_BLOCK_TYPE` | Unknown block type in blocks input: \{type} | The blocks document has a block of type '\{type}', which this engine version does not know. A newer engine may have saved the blocks. Upgrade the engine, or save the blocks again without that block. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.BLOCK_AT_INDEX_INVALID` | Block at index \{index} is invalid. Can't save list. | One of the blocks in the list was destroyed. Filter or re-resolve the ids before saving. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.COMPRESSION_SERVICE_UNAVAILABLE` | Compression service not available for detected format. | The archive is compressed with a codec this build does not support. Ensure the engine was built with the matching compression backend. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.CONTENT_EMPTY` | Received empty \{kind} content. | The serialized \{kind} payload is empty. Verify the source produced non-empty content before passing it to the engine. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.DECOMPRESS_FAILED` | Failed to decompress scene data: \{reason} | The compressed payload could not be decoded. The archive may be truncated or produced by an incompatible writer. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.DISALLOWED_SCHEMES` | Scene contains disallowed schemes in resource URLs: \{schemes} | Add \{schemes} to allowedResourceSchemes when you save, or pass a persistence callback that uploads these resources. A data: URI needs 'data' in allowedResourceSchemes. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ENTITY_INVALID` | Invalid scene entity. | The provided design-block id no longer references a valid entity. It may have been destroyed by an earlier call; re-resolve the id before use. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ENTITY_NOT_A_SCENE` | Entity is not a scene. | Pass a scene block id. Use api.scene.get() or api.block.findByType('//ly.img.ubq/scene') to obtain one. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.FRAME_RATE_OUT_OF_RANGE` | Invalid scene frame rate \{value}. It must be greater than 0 and at most \{max}. | Set scene/frameRate to a value in (0, \{max}] frames per second, such as 24, 30 or 60. | | | `SCENE.LOAD_FROM_URI_FAILED` | Could not load \{kind} from \{uri}. | Check that the URI is reachable and serves a valid \{kind} payload. Common causes: 404, CORS, or wrong file type. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.LOTTIE_ARCHIVE_INVALID` | The dotLottie archive is invalid: \{reason}. | Pass a valid .lottie file: a zip archive whose manifest.json lists an animation that the archive contains. | | | `SCENE.LOTTIE_ARCHIVE_TOO_LARGE` | The dotLottie archive holds more than \{limit} bytes when uncompressed. | Reduce the size of the animations and images in the .lottie archive. | | | `SCENE.LOTTIE_ARCHIVE_TOO_MANY_ENTRIES` | The dotLottie archive has \{count} entries, more than the limit of \{limit}. | Remove unused files from the .lottie archive. | | | `SCENE.LOTTIE_ARCHIVE_UNSAFE_PATH` | The dotLottie archive has an entry outside the archive: \{path}. | Use a .lottie file whose entry names are relative paths without '..' segments. | | | `SCENE.MARKER_INDEX_OUT_OF_RANGE` | No marker at index \{index}. | Pass an index into the list of scene markers. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.MARKER_INVALID_TIME` | Marker time and duration must be finite and not negative. | Pass a finite time and duration in seconds, both 0 or greater. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.MEDIA_URI_NOT_FOUND` | Could not load \{kind} from \{uri}. | The \{kind} resource at \{uri} could not be resolved. Check the URI, network connectivity, and asset-source registrations. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.MEDIA_URI_PARSE_FAILED` | Could not load \{kind}: \{reason} | The \{kind} URI failed to parse. Confirm it is a well-formed absolute URI (http(s)://, file://, or a data URL). | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.MUST_EXIST` | A scene must already exist. | Create or load a scene before invoking this API. Use api.scene.create() or loadFromString(). | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.MUST_EXIST_FOR_TEMPLATE` | A scene must already exist for a template to be applied to it. | Load a scene first, then apply the template. Templates only modify existing scenes. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.NOT_IMPLEMENTED` | Not implemented. | This data-provider method has no implementation on the active subclass. The caller must select a different provider or operation. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.NOT_SCENE_TYPE` | Not a scene. | The root deserialized entity is not a scene. The file may have been produced by saveAsBlocks() — load it with loadBlocks() instead. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.NOT_VALID` | No valid scene. | Create or load a scene before calling APIs that operate on the active scene. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.NO_MODE` | Scene has no mode set. | Use api.scene.setMode(scene, mode) before requesting mode-dependent behavior. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.NO_PAGES_FOR_AUDIO_EXPORT` | Scene has no pages to export audio from. | Add at least one page to the scene before exporting audio. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.NO_PAGE_FOUND` | No page found. | Add at least one page to the scene or check the current viewport's page resolution. | [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) | | `SCENE.NO_SCENE_FOUND` | No scene found. | The serialized payload deserialized successfully but contains no scene root. Was the file saved as 'blocks' instead of 'scene'? | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.PERSISTENCE_NO_URL` | The persistence callback answered with no URL for \{url} | Answer with the URL where you stored the resource, as a string, or answer with an error. | [Resources](https://img.ly/docs/cesdk/mac-catalyst/concepts/resources-a58d71/) | | `SCENE.TEMP_FILE_CREATE_FAILED` | Failed to create temporary file. | The engine could not create a scratch file for archival. Check available disk space and the platform's temporary-directory permissions. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_CHUNK_WRITE_FAILED` | Failed to write chunk to ZIP entry: \{zipErrorCode} | libzip rejected writing a chunk to the current streaming entry with error code \{zipErrorCode}. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_CREATE_FAILED` | Failed to create ZIP archive. | The ZIP container could not be opened for writing. This is usually a file-system or memory issue. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_DIRECTORY_ENTRY_CLOSE_FAILED` | Failed to close ZIP directory entry: \{zipErrorCode} | libzip rejected closing a directory entry with error code \{zipErrorCode}. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_DIRECTORY_ENTRY_OPEN_FAILED` | Failed to open ZIP directory entry: \{zipErrorCode} | libzip rejected opening a directory entry with error code \{zipErrorCode}. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_ENTRY_CLOSE_FAILED` | Failed to close ZIP entry: \{zipErrorCode} | libzip rejected closing the current entry with error code \{zipErrorCode}. The entry data may be truncated. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_ENTRY_OPEN_FAILED` | Failed to open ZIP entry: \{zipErrorCode} | libzip rejected opening a new entry with error code \{zipErrorCode}. The archive state may be inconsistent. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_ENTRY_WRITE_FAILED` | Failed to write ZIP entry: \{zipErrorCode} | libzip rejected writing bytes to the current entry with error code \{zipErrorCode}. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_STREAMING_ENTRY_CLOSE_FAILED` | Failed to close ZIP entry for streaming resource: \{zipErrorCode} | libzip rejected closing a streaming entry with error code \{zipErrorCode}. The streamed bytes may be truncated. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_STREAMING_ENTRY_OPEN_FAILED` | Failed to open ZIP entry for streaming resource: \{zipErrorCode} | libzip rejected opening a streaming entry with error code \{zipErrorCode}. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_WRITER_CLOSE_FAILED` | Failed to close ZIP writer: \{zipErrorCode} | libzip reported error \{zipErrorCode} on close. The output bytes may be truncated; treat the archive as invalid. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_WRITER_CREATE_FAILED` | Failed to create ZIP writer. | The libzip writer could not be allocated. Out-of-memory or platform-resource exhaustion is the likely cause. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | | `SCENE.ZIP_WRITER_OPEN_FAILED` | Failed to open ZIP writer: \{zipErrorCode} | libzip rejected the writer initialization with error code \{zipErrorCode}. Inspect libzip's documentation for the meaning. | [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) | ## UTILS Generic utility failures not specific to another category. | Code | Message | Hint | Docs | | --- | --- | --- | --- | | `UTILS.APNG_DATA_TOO_SMALL` | APNG data is empty or too small. | The APNG buffer is too short to contain a valid PNG signature. The source may be truncated. | | | `UTILS.APNG_FCTL_BEFORE_ACTL` | APNG has fcTL before acTL. | fcTL chunks must follow acTL. Reorder or regenerate the APNG. | | | `UTILS.APNG_FDAT_WITHOUT_FCTL` | APNG has fdAT with no preceding fcTL. | Each fdAT chunk must follow an fcTL declaring its frame. The animation stream is broken. | | | `UTILS.APNG_FDAT_WITHOUT_SEQ` | APNG has an fdAT chunk without a sequence number. | fdAT chunks must include a 4-byte sequence number. The chunk is malformed. | | | `UTILS.APNG_FRAME_INDEX_OUT_OF_RANGE` | APNG frame index out of range. | The requested frame index is past the last frame. Clamp it to \[0, frameCount). | | | `UTILS.APNG_FRAME_NO_PIXEL_DATA` | APNG has an animation frame with no pixel data. | Each animation frame must have at least one IDAT/fdAT chunk. | | | `UTILS.APNG_FRAME_OUT_OF_CANVAS` | APNG frame rectangle extends beyond the canvas. | fcTL coordinates must fit inside the canvas declared by IHDR. The file is malformed. | | | `UTILS.APNG_FRAME_ZERO_DIMENSIONS` | APNG has an animation frame with zero dimensions. | Each fcTL must declare positive width and height. The frame is invalid. | | | `UTILS.APNG_IDAT_BEFORE_IHDR` | APNG has IDAT before IHDR. | IDAT chunks must follow IHDR. The file violates PNG chunk ordering. | | | `UTILS.APNG_INVALID_ACTL` | APNG has invalid or misplaced acTL chunk. | The acTL chunk must precede IDAT and appear exactly once. The animation chunk is malformed. | | | `UTILS.APNG_INVALID_FCTL` | APNG has an invalid fcTL chunk. | An animation control chunk failed validation. The file may be partially corrupted. | | | `UTILS.APNG_INVALID_IHDR` | APNG has invalid or duplicated IHDR chunk. | PNG must contain exactly one IHDR chunk at the start. The file is malformed. | | | `UTILS.APNG_INVALID_SIGNATURE` | APNG data does not start with a valid PNG signature. | The first 8 bytes must be the PNG magic number. The source is not a PNG/APNG file. | | | `UTILS.APNG_MISSING_ACTL` | Not an APNG: acTL chunk is missing. | This is a static PNG, not an APNG. Use it with a single-frame decode path or supply an animated file. | | | `UTILS.APNG_MISSING_IHDR` | APNG source has no valid IHDR chunk. | Every PNG starts with IHDR. The file is empty or corrupted. | | | `UTILS.APNG_NO_FRAMES` | APNG declares no animation frames. | The acTL reports zero frames. Regenerate the APNG with at least one fcTL/fdAT. | | | `UTILS.APNG_ZERO_CANVAS` | APNG has zero canvas dimensions. | Canvas width and height must be > 0. The IHDR is corrupt. | | | `UTILS.BASE_PATH_INVALID` | The base path '\{basePath}' is neither an absolute file path nor a URI. | Start the engine with an absolute file path or a URL as the base path. In the path of a URL, percent-encode each character that a URI cannot hold, such as '\[' or a non-ASCII letter. | | | `UTILS.CAPTION_DATA_UNAVAILABLE` | Caption data not available. | The caption resource has no bytes registered. Ensure data is loaded before requesting captions. | | | `UTILS.CAPTION_PARSE_EMPTY` | Failed to parse captions, no captions found. | The caption file parsed but contained no entries. Verify the source format. | | | `UTILS.CAPTION_UNSUPPORTED_MIME` | Unsupported caption mime type: \{mimeType} | Caption format '\{mimeType}' is not recognized. Convert to a supported format (e.g. text/vtt, application/x-subrip). | | | `UTILS.CAPTION_UTF16_INVALID_SIZE` | Failed to parse captions, unexpected file size for UTF-16BE encoded text. | UTF-16BE text must have an even byte count. The file is truncated. | | | `UTILS.COMPRESSION_EMPTY_DATA` | Cannot decompress empty data. | The input buffer is empty. Pass at least one byte of compressed data. | | | `UTILS.COMPRESSION_FORMAT_NONE_FOR_COMPRESS` | Cannot compress data with format None: no compression requested. | Pass a non-None compression format if compression is intended. | | | `UTILS.COMPRESSION_FORMAT_NONE_FOR_DECOMPRESS` | Cannot decompress data with format None: data is not compressed. | The detected format is None — the data is not compressed and does not need decompression. | | | `UTILS.COMPRESSION_FORMAT_UNSUPPORTED` | Unsupported compression format: \{format} | Compression format \{format} is not handled by this build. | | | `UTILS.COMPRESSION_NO_MAGIC_BYTES` | Unable to detect compression format: data does not have valid compression magic bytes. | The first bytes do not match any supported compression format. The data may not be compressed. | | | `UTILS.COMPRESSION_ZSTD_INVALID_FRAME` | Invalid zstd frame. | The zstd decoder rejected the frame header. The data may be truncated. | | | `UTILS.ENGINE_UNKNOWN_COMPONENT_TYPE` | Component \{component} is not a known reflected type. | Component '\{component}' is not registered with the reflection system. | | | `UTILS.ENUM_VALUE_INVALID` | Invalid enum value, expected one of: \{validValues} | The supplied string is not a member of the target enum. Pass one of: \{validValues}. | | | `UTILS.FILE_ALLOC_FAILED` | Could not allocate an output buffer of size \{size} to load \{path} | Out-of-memory loading '\{path}' (\{size} bytes). Free memory or stream the file in chunks. | | | `UTILS.FILE_MAP_FAILED` | Could not map file \{path} into memory: \{reason} | mmap() rejected '\{path}': \{reason}. Out-of-memory or virtual-memory limits may be the cause. | | | `UTILS.FILE_OPEN_FAILED` | Could not open file \{path}: \{reason} | The file '\{path}' could not be opened. Reason: \{reason} | | | `UTILS.FILE_READ_FAILED` | Could not read complete file \{path}: \{reason} | Reading '\{path}' returned an error or hit EOF prematurely. Reason: \{reason} | | | `UTILS.FILE_SIZE_FAILED` | Could not determine file size for \{path}: \{reason} | stat() failed on '\{path}': \{reason}. The file may have been removed or is unreadable. | | | `UTILS.FILE_TOO_LARGE` | File \{path} has size \{size} larger than the maximum supported \{max} | Loading is capped at \{max} bytes. Use streaming APIs for files larger than this. | | | `UTILS.GIF_PARSE_FAILED` | Failed to parse GIF file: \{reason} | The GIF source could not be decoded. Underlying reason: \{reason} | | | `UTILS.METAANY_EXPECTED_ARRAY` | Expected array, but got \{actual} | JS value must be an array. Got '\{actual}' instead. | | | `UTILS.METAANY_ITEM_MAP_FAILED` | Couldn't map item at index \{index} > \{reason} | Array element \{index} failed conversion. Reason: \{reason} | | | `UTILS.METAANY_MEMBER_MAP_FAILED` | Couldn't map value for member \`\{member}\` > \{reason} | Member '\{member}' failed conversion. Reason: \{reason} | | | `UTILS.METAANY_MISSING_PROPERTY` | Expected object of type '\{type}' to have property '\{property}'. | The JS object is missing required property '\{property}'. Add it before passing across the boundary. | | | `UTILS.METAANY_NON_STRING_TO_STRING` | Can't map non-string em::val to string. | The JavaScript value is not a string. Convert it to a string in JS before crossing the boundary. | | | `UTILS.METAANY_RESULT_TYPE_NOT_REFLECTED` | Type of Result value for \{details} is not reflected. | The Result type returned from '\{details}' is not reflection-registered. | | | `UTILS.METAANY_SET_FAILED` | Could not set value for \{member} on object of type \{type} | The reflected setter for '\{member}' on '\{type}' rejected the value. | | | `UTILS.METAANY_TYPE_NEEDS_REFLECTION` | Type \{type} must have reflection info. | Register type '\{type}' with the reflection system before crossing the JS↔C++ boundary. | | | `UTILS.METAANY_UNDEFINED` | Can't map \`undefined\` to \`\{type}\` | JavaScript \`undefined\` cannot be converted to '\{type}'. Pass a defined value of the expected type. | | | `UTILS.METAANY_UNHANDLED_INTEGRAL` | Unhandled integral type: \{type} | The integral type '\{type}' has no MetaAny ↔ em::val mapping. | | | `UTILS.METAANY_UNHANDLED_SEQUENCE` | Unhandled sequence container type: \{type} | Sequence container '\{type}' has no MetaAny ↔ em::val mapping. | | | `UTILS.METAANY_UNHANDLED_TYPE_KIND` | Unhandled type kind. \{type} | Type kind for '\{type}' is not recognized by the conversion layer. | | | `UTILS.META_TYPE_FUNCTION_UNKNOWN` | Function must be known. | The requested reflected function is not registered on this MetaType. | | | `UTILS.META_TYPE_INVALID` | Not a valid type, no name available. | The MetaType handle does not refer to a registered reflection type. | | | `UTILS.META_TYPE_INVOKE_FAILED` | Could not invoke \{name}. | The reflected function '\{name}' could not be invoked. Argument types or arity may not match. | | | `UTILS.MKV_EBML_PARSE_FAILED` | Failed to parse EBML header. | The EBML header is malformed. The file may not be a Matroska container. | | | `UTILS.MKV_INVALID_DATA` | Invalid MKV data. | The MKV demuxer rejected the source bytes. The file may be truncated or use an unsupported variant. | | | `UTILS.MKV_NO_TRACKS` | No tracks found. | The MKV file declares no tracks. Verify it contains video/audio streams. | | | `UTILS.MKV_SEGMENT_CREATE_FAILED` | Failed to create segment. | libwebm could not allocate a segment. Out-of-memory or unsupported MKV variant. | | | `UTILS.MKV_SEGMENT_LOAD_FAILED` | Failed to load segment. | The MKV segment payload is unreadable. The file may be truncated mid-segment. | | | `UTILS.MP4_AUDIO_CODEC_UNSUPPORTED` | Unsupported audio codec '\{fourcc}' (OTI=\{oti}). | MP4 audio must use the AAC (mp4a), MP3, AMR (samr/sawb), or Opus codec. Re-encode the audio to AAC. | | | `UTILS.MP4_AUDIO_TRACK_INDEX_OUT_OF_BOUNDS` | Audio track index \{index} is out of bounds. Valid range: 0-\{max} | Pass an audio track index within \[0, \{max}]. | | | `UTILS.MP4_OPEN_FAILED` | Failed to open MP4 file. | The MP4 demuxer could not open the source. The file may be missing or unreadable. | | | `UTILS.MP4_SAMPLE_OUTSIDE_FILE` | A sample of the MP4 file lies outside the file. | The file is truncated or its sample tables are damaged. Export or download the file again. | | | `UTILS.MP4_VIDEO_CODEC_UNSUPPORTED` | Unsupported video codec '\{fourcc}' (OTI=\{oti}). | MP4 video must use the H.264 (avc1) or H.265 (hvc1) codec. Re-encode the video to H.264 or H.265, or use a WebM file for AV1, VP8, or VP9. | | | `UTILS.PIXEL_BUFFER_BACKEND_TEXTURE_INCOMPLETE` | Backend texture is incomplete. | The Skia backend texture handle does not carry enough state for read-back. | | | `UTILS.PIXEL_BUFFER_GL_TEXTURE_INFO_FAILED` | Failed to retrieve OpenGL texture info. | The backend texture did not expose its GL info. The compute context may not be a GL context. | | | `UTILS.PIXEL_BUFFER_INVALID_IMAGE` | Invalid RasterImage or SkImage. | The image handle is null or not a recognized Skia/raster image. | | | `UTILS.PIXEL_BUFFER_NOT_STREAM_FILL` | This operation is only supported for pixel stream fills. | Use a block with a pixel-stream fill type. Standard fills are not supported by this conversion API. | | | `UTILS.PIXEL_BUFFER_NO_BACKEND_TEXTURE` | No valid backend texture. | The Skia image is not backed by a GPU texture. The current compute context may not support direct read-back. | | | `UTILS.PIXEL_BUFFER_NO_CANVAS` | No canvas. | The Skia surface has no canvas attached. The compute context may not be initialized. | | | `UTILS.PIXEL_BUFFER_NO_GPU_CONTEXT` | No GPU context. | The compute context lacks a GPU backend. Pixel-buffer conversion requires GPU access. | | | `UTILS.PIXEL_BUFFER_UNSUPPORTED_FORMAT` | Unsupported pixel format. | The native pixel buffer is not 32-bit BGRA. Convert the source to kCVPixelFormatType\_32BGRA before updating the pixel-stream fill. | | | `UTILS.PIXEL_BUFFER_UNSUPPORTED_PLATFORM` | Native pixel buffers are not supported on this platform. | Linux and Windows builds cannot update a pixel-stream fill from a native buffer. Call this on iOS, macOS, Android or the web. | | | `UTILS.REFLECTION_BLOCK_NOT_VALID` | The block with ID \{block} is not valid. It may have been deleted, e.g., when a new scene was loaded. | Resolve the block id before invoking reflection APIs. Block ids become invalid after scene reloads or block destruction. | | | `UTILS.REFLECTION_COMPONENT_NOT_REFLECTED` | '\{component}' is not a reflected type (tried to get '\{memberPath}' member). | Register the component with the reflection system or pass an already-registered type name. | | | `UTILS.REFLECTION_COMPONENT_NOT_SET` | Component \{component} is not set on entity \{entity}. | Verify the entity has been assigned the component before reading. Use the entity's component-list API to inspect. | | | `UTILS.REFLECTION_ENTITY_MISSING_COMPONENT` | Entity ID \{entity}\{optionalType} doesn't have component "\{component}". | The block does not carry this component. Use a block type that supports it or attach the component first. | | | `UTILS.REFLECTION_KEY_NOT_FOUND` | Could not find member "\{key}" | The key '\{key}' does not exist on the current type. Inspect available members and adjust. | | | `UTILS.REFLECTION_KEY_PATH_EMPTY` | Key path must be at least 1 element but is empty. | Provide a non-empty key path (e.g. "transform/x"). | | | `UTILS.REFLECTION_KEY_PATH_MEMBER_MISSING` | Member "\{member}" for type "\{type}" not found for given key path "\{keyPath}". Valid members for key path "\{prefix}*" are: \{members}. | Adjust '\{keyPath}' to reference one of the listed members. | | | `UTILS.REFLECTION_MEMBER_NOT_ACCESSIBLE_BY_REF` | Could not get member "\{member}". Must access by reference, if not modifying the most nested member. Reflected members for key path "\{prefix}*" are: \{members}. | Intermediate segments of a key path require by-reference access. Mark '\{member}' as a by-reference member or address its tail directly. | | | `UTILS.REFLECTION_MEMBER_NOT_FOUND` | Member \{member} not found on current type. | The member '\{member}' is not a reflected field on this type. Check the type's reflected members. | | | `UTILS.REFLECTION_MEMBER_TYPE_NOT_REFLECTED` | Type of member named "\{member}" on "\{type}" is not reflected. Reflected members for key path "\{prefix}*" are: \{members}. | Reflection cannot descend into '\{member}' because its type is not registered. Reflect the type or stop the key path at '\{member}'. | | | `UTILS.REFLECTION_NO_MEMBERS_FOR_PREFIX` | "\{member}" has no reflected members. | The intermediate type at '\{member}' has no fields to descend into. Adjust the key path. | | | `UTILS.REFLECTION_SET_MEMBER_FAILED` | Could not set member "\{member}" of type "\{type}". | The reflected setter for '\{member}' returned false. Verify the value type matches the member. | | | `UTILS.REFLECTION_SET_WILDCARD_TYPE_MISMATCH` | Type mismatch between "\{lhs}" and "\{rhs}": "\{lhsType}" vs. "\{rhsType}". | Wildcard set requires all members to share a type. | | | `UTILS.REFLECTION_TYPE_NOT_REFLECTED` | \{component} is not a reflected type. | Register the type with the reflection system before accessing it dynamically. | | | `UTILS.REFLECTION_TYPE_NO_MEMBERS` | Type "\{type}" has no reflected members. | The wildcard key path requires a type with reflected fields. Use a concrete member name or pick a different type. | | | `UTILS.REFLECTION_UNSUPPORTED_TYPE` | Unsupported type. | The reflected type is not handled by this operation. Pass a supported type. | | | `UTILS.REFLECTION_WILDCARD_NOT_TAIL` | Wildcard only supported at tail end of path. | Move the wildcard '*' to the last segment of the key path. | | | `UTILS.REFLECTION_WILDCARD_SET_PARTIAL` | Could not set all members of type "\{type}". | At least one reflected member of '\{type}' rejected the assignment. Member-level setters may have failed silently. | | | `UTILS.REFLECTION_WILDCARD_TYPE_MISMATCH` | Type mismatch between "\{lhs}" and "\{rhs}". | Wildcard get requires all matched members to share a type. Members '\{lhs}' and '\{rhs}' differ. | | | `UTILS.REFLECTION_WILDCARD_VALUE_MISMATCH` | Value mismatch between "\{lhs}" and "\{rhs}". | Wildcard get returns a single representative value; members '\{lhs}' and '\{rhs}' currently disagree. | | | `UTILS.SETTING_NO_ARGS` | Received no arguments. Can't determine type. | The setting's type is inferred from the value you pass, so the call cannot accept zero arguments. Pass the setting value explicitly (bool, int, float, color, or string). | | | `UTILS.STD_EXPECTED_STRING_ERROR` | \{message} | Adapter path between \`std::expected\\` and \`Result\\`. The source returned a free-text \`std::string\` error — surfacing it through the catalog so consumers still see the original wording. Convert the producer to return a catalog id when possible. | | | `UTILS.STORAGE_KEY_NOT_FOUND` | Key not found. | The requested storage key has no value. Write it before reading, or guard the read by checking for existence first. | | | `UTILS.TRACKING_ENDPOINT_OVERRIDE_UNAVAILABLE` | This build of the engine cannot change the tracking endpoint. | A release build always sends usage events to IMG.LY. Remove the override, such as the \`ly.img.engine.OVERRIDE\_TRACKING\_ENDPOINT\` entry in an Android manifest. | | | `UTILS.TRACKING_NOT_ENABLED` | Tracking is not enabled. | Enable tracking on the engine before invoking tracking APIs. | | | `UTILS.URI_NON_ASCII` | Failed to parse URI: \{uri}. Contains non-ASCII byte 0x\{byte} at position \{position}. URIs must only contain ASCII characters, please percent-encode special characters. | Percent-encode special characters in '\{uri}' before parsing. Non-ASCII byte 0x\{byte} found at position \{position}. | | | `UTILS.URI_PARSE_FAILED` | Failed to parse URI: \{uri} | The URI '\{uri}' is malformed. Confirm scheme, host, and path. | | | `UTILS.WAV_DATA_BEFORE_FMT` | Invalid WAV file: data chunk found before fmt chunk. | WAV chunk order requires fmt before data. Re-encode the file. | | | `UTILS.WAV_FMT_CHUNK_TOO_SMALL` | WAV fmt chunk too small: expected at least \{expected} bytes, got \{actual}. | The fmt chunk does not carry the minimum WAV header fields. | | | `UTILS.WAV_INVALID_BITS_PER_SAMPLE` | Invalid WAV bits\_per\_sample: \{bitsPerSample} | WAV bits\_per\_sample must be one of 8/16/24/32. Got \{bitsPerSample}. | | | `UTILS.WAV_MISSING_FMT_OR_DATA` | Invalid WAV file: missing fmt or data chunk. | Both fmt and data chunks are required. The file is missing one or both. | | | `UTILS.WAV_NOT_WAVE` | Not a WAV file: RIFF format is not WAVE. | The RIFF container is not a WAVE file. The source is a different RIFF type or not RIFF at all. | | | `UTILS.WAV_TRUNCATED_FMT_CHUNK` | WAV file truncated: not enough data for fmt chunk. | The fmt chunk is cut off. The file is incomplete. | | | `UTILS.WAV_TRUNCATED_RIFF_HEADER` | WAV file truncated: not enough data for RIFF header. | The file is shorter than the 12-byte RIFF header. The source is incomplete. | | | `UTILS.WAV_TRUNCATED_UNKNOWN_CHUNK` | WAV file truncated: not enough data to skip unknown chunk. | Parsing tried to skip an unknown chunk but ran past the buffer end. The file is incomplete. | | | `UTILS.WAV_UNSUPPORTED_AUDIO_FORMAT` | Unsupported WAV audio format: \{audioFormat} | WAV audio format code \{audioFormat} is not handled. Re-encode as PCM (1) or IEEE float (3). | | | `UTILS.WAV_ZERO_CHANNELS` | Invalid WAV file: num\_channels is 0. | WAV must declare at least one channel. | | | `UTILS.WAV_ZERO_SAMPLE_RATE` | Invalid WAV file: sample\_rate is 0. | WAV must declare a positive sample rate. | | | `UTILS.WAV_ZERO_SIZE_CHUNK` | Invalid WAV file: zero-size chunk encountered. | A chunk reports zero size, which would cause infinite parsing. The file is malformed. | | | `UTILS.WEBM_MUX_FAILED` | Could not write the WebM file: \{reason}. | The WebM muxer refused the export. Export again, and report the reason if it repeats. | | | `UTILS.ZSTD_COMPRESS_FAILED` | Zstd compression failed: \{reason} | The zstd library reported a compression failure. \`reason\` is the value returned by \`ZSTD\_getErrorName(...)\`. | | | `UTILS.ZSTD_DECOMPRESS_FAILED` | Zstd decompression failed: \{reason} | The zstd library reported a decompression failure. \`reason\` is the value returned by \`ZSTD\_getErrorName(...)\`. The input may be truncated or not zstd-encoded. | | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Events" description: "Subscribe to block creation, update, and deletion events to track changes in your CE.SDK scene." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/events-353f97/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Events](https://img.ly/docs/cesdk/mac-catalyst/concepts/events-353f97/) --- ```swift file=@cesdk_swift_examples/engine-guides-events/Events.swift reference-only import Foundation import IMGLYEngine @MainActor // swiftlint:disable:next cyclomatic_complexity func events(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.star)) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.appendChild(to: page, child: block) let allEventsTask = Task { for await events in engine.event.subscribe(to: []) { for event in events { print("Event: \(event.type) for block \(event.block)") } } } let specificTask = Task { for await events in engine.event.subscribe(to: [block]) { for event in events { print("Specific event: \(event.type) for block \(event.block)") } } } try await Task.sleep(nanoseconds: NSEC_PER_SEC) let processTask = Task { for await events in engine.event.subscribe(to: []) { for event in events { switch event.type { case .created: let type = try engine.block.getType(event.block) print("Block created: \(type)") case .updated: let type = try engine.block.getType(event.block) print("Block updated: \(type)") case .destroyed: print("Block destroyed: \(event.block)") @unknown default: break } } } } try await Task.sleep(nanoseconds: NSEC_PER_SEC) try engine.block.setRotation(block, radians: 0.5 * .pi) try await Task.sleep(nanoseconds: NSEC_PER_SEC) if engine.block.isValid(block) { let type = try engine.block.getType(block) print("Block is valid: \(type)") } try engine.block.destroy(block) try await Task.sleep(nanoseconds: NSEC_PER_SEC) allEventsTask.cancel() specificTask.cancel() processTask.cancel() } ``` Monitor and react to block changes in real time by subscribing to creation, update, and destruction events in your CE.SDK scene. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-events) Events enable real-time monitoring of block changes in CE.SDK. When blocks are created, modified, or destroyed, the engine delivers these changes through subscriptions at the end of each update cycle. In Swift, the event API uses `AsyncStream` for seamless integration with Swift concurrency. This guide covers subscribing to block lifecycle events, processing the three event types (`.created`, `.updated`, `.destroyed`), filtering events to specific blocks, and properly cleaning up subscriptions. ## Setup Create a scene with a graphic block to observe events on: ```swift highlight-events-setup let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.star)) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.appendChild(to: page, child: block) ``` ## Event Types CE.SDK provides three event types that capture the block lifecycle: | Type | Description | |------|-------------| | `.created` | Fires when a new block is added to the scene | | `.updated` | Fires when any property of a block changes | | `.destroyed` | Fires when a block is removed from the scene | Each `BlockEvent` contains a `block` property with the block ID and a `type` property indicating which event occurred. ## Subscribing to All Blocks Use `engine.event.subscribe(to:)` to receive an `AsyncStream` of batched events. Pass an empty array to receive events from all blocks in the scene: ```swift highlight-events-subscribeAll let allEventsTask = Task { for await events in engine.event.subscribe(to: []) { for event in events { print("Event: \(event.type) for block \(event.block)") } } } ``` Iterate the stream inside a `Task`. The stream emits arrays of `BlockEvent` at the end of each engine update cycle. ## Subscribing to Specific Blocks For better performance when you only care about certain blocks, pass an array of block IDs to filter events: ```swift highlight-events-subscribeSpecific let specificTask = Task { for await events in engine.event.subscribe(to: [block]) { for event in events { print("Specific event: \(event.type) for block \(event.block)") } } } ``` This reduces overhead since the engine only prepares events for the blocks you're tracking. ### API Reference ```swift public func subscribe(to blocks: [DesignBlockID]) -> AsyncStream<[BlockEvent]> ``` Subscribe to block life-cycle events. - `blocks:`: A list of blocks to filter events by. If the list is empty, events for every block are sent. - Returns: A stream of events. Events are bundled and sent at the end of each engine update. ## Processing Events by Type Handle each event type by switching on the `type` property. For `.created` and `.updated` events, you can safely use Block API methods. For `.destroyed` events, the block ID is no longer valid: ```swift highlight-events-processEvents let processTask = Task { for await events in engine.event.subscribe(to: []) { for event in events { switch event.type { case .created: let type = try engine.block.getType(event.block) print("Block created: \(type)") case .updated: let type = try engine.block.getType(event.block) print("Block updated: \(type)") case .destroyed: print("Block destroyed: \(event.block)") @unknown default: break } } } } ``` ## Triggering Events Modifying any property of a block triggers an `.updated` event. Due to deduplication, you receive at most one `.updated` event per block per engine update cycle, regardless of how many properties changed: ```swift highlight-events-updated try engine.block.setRotation(block, radians: 0.5 * .pi) ``` Destroying a block triggers a `.destroyed` event: ```swift highlight-events-destroyed try engine.block.destroy(block) ``` ## Handling Destroyed Blocks Safely When a block is destroyed, its ID becomes invalid. Calling Block API methods on a destroyed block throws an error. Always check validity with `engine.block.isValid()` before operations: ```swift highlight-events-destroyedSafety if engine.block.isValid(block) { let type = try engine.block.getType(block) print("Block is valid: \(type)") } ``` ## Unsubscribing from Events In Swift, cancelling the `Task` that iterates the `AsyncStream` automatically unsubscribes from events. Cancel tasks when you no longer need to track changes: ```swift highlight-events-unsubscribe allEventsTask.cancel() specificTask.cancel() processTask.cancel() ``` Always cancel subscription tasks when your view disappears or you no longer need to track changes. Keeping unnecessary subscriptions active forces the engine to prepare event lists at every update. > **Tip:** CE.SDK also provides a Combine-based `engine.event.publisher(blocks:)` method as an alternative to `AsyncStream` for Combine-based architectures. ## Event Batching and Deduplication Events are collected during an engine update and delivered together at the end. The engine deduplicates events, so you receive at most one `.updated` event per block per update cycle. Event order in the stream does not reflect the actual order of changes within the update. This batching behavior means: - Multiple property changes to a single block result in one `.updated` event - You cannot determine which specific property changed from the event alone - If you need to track specific property changes, compare against cached values ## Next Steps - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Learn about block types, properties, and lifecycle. - [Undo and History](https://img.ly/docs/cesdk/mac-catalyst/concepts/undo-and-history-99479d/) — Implement undo/redo functionality. - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) — Understand scene structure and management. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Exclusion Areas" description: "Mark a region of a page that content must stay out of, and cut it out of an export." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/exclusion-areas-c2b87b/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Exclusion Areas](https://img.ly/docs/cesdk/mac-catalyst/concepts/exclusion-areas-c2b87b/) --- An exclusion area marks a region of a page that content must stay clear of — an envelope window, a book spine, a glue flap, an address panel on a mailer. This guide covers creating an exclusion area, putting the artwork of the obstruction into it, controlling how it looks on the canvas, and cutting it out of an export. An exclusion area is authoring geometry. It is visible while you design and stays out of the exported file, so a region you must respect no longer has to be faked with a locked graphic. ## Creating an Exclusion Area Create an exclusion area with the `exclusionArea` block type and append it to a page: ```swift let exclusionArea = try engine.block.create(.exclusionArea) try engine.block.setPositionX(exclusionArea, value: 20) try engine.block.setPositionY(exclusionArea, value: 20) try engine.block.setWidth(exclusionArea, value: 40) try engine.block.setHeight(exclusionArea, value: 40) try engine.block.appendChild(to: page, child: exclusionArea) ``` A new exclusion area is a rectangle with a stripe fill (`//ly.img.ubq/fill/stripe`), so it is visible as soon as you create it. The stripes leave their gaps transparent, so the exclusion area marks the region without hiding what is behind it. An exclusion area is a graphic. It takes a shape, a fill, a stroke, effects and a blur, so you can put the artwork of the obstruction into it. Assigning another fill replaces the stripes: ```swift let fill = try engine.block.createFill(.image) try engine.block.setString( fill, property: "fill/image/imageFileURI", value: "https://img.ly/static/ubq_samples/sample_1.jpg" ) try engine.block.setFill(exclusionArea, fill: fill) ``` Because an exclusion area takes any shape a graphic takes, a round window or a die cut outline is an exclusion area with that shape assigned: ```swift try engine.block.setShape(exclusionArea, shape: engine.block.createShape(.ellipse)) ``` ## Appearance The engine washes every exclusion area in `page/exclusionAreaFillColor` and frames it in `page/exclusionAreaFrameColor`. ```swift try engine.editor.setSettingColor( "page/exclusionAreaFillColor", r: 0.79, g: 0.12, b: 0.4, a: 0.35 ) ``` Both are editor settings rather than block properties, so they apply to every exclusion area in the scene and neither reaches an export. A fully transparent `page/exclusionAreaFrameColor` hides the frame, and a fully transparent `page/exclusionAreaFillColor` hides the wash. The stripes are the exclusion area's fill, so neither setting changes them. Reach the stripes of one exclusion area through its fill: ```swift let stripes = try engine.block.getFill(exclusion area) try engine.block.setColor( stripes, property: "fill/stripe/color", r: 0.79, g: 0.12, b: 0.4, a: 0.25 ) try engine.block.setFloat(stripes, property: "fill/stripe/width", value: 4) try engine.block.setFloat(stripes, property: "fill/stripe/gap", value: 4) try engine.block.setFloat(stripes, property: "fill/stripe/angle", value: .pi / 2) ``` `fill/stripe/width` and `fill/stripe/gap` are in pixels at the resolution of the scene, so they keep their size when you scale the exclusion area or change the design unit, and `fill/stripe/angle` is in radians, where 0 stands the stripes upright and `.pi / 2` lays them flat. The default is `.pi / 4`. ## Exclusion Areas and Export An exclusion area is authoring state, so it is left out of an export. Set `includedInExport` to `true` on the exclusion area to put its artwork in the file. The guide colors still stay out: ```swift try engine.block.setIncludedInExport(exclusionArea, enabled: true) ``` The stripes are the exclusion area's fill, so an exclusion area that still has the fill it was created with brings its stripes into the file. Assign the artwork of the obstruction to the exclusion area to export that in place of the stripes. Set `exclusionArea/punchOut` to `true` to cut the exclusion area out of an export instead. The page and everything on it get a hole where the exclusion area is, so a die cut window in the design becomes a window in the exported file: ```swift try engine.block.setBool(exclusionArea, property: "exclusionArea/punchOut", value: true) ``` The hole is transparent, which a print workflow reads as an absence of ink rather than as white. Punch-out is off by default, and the canvas keeps showing the content either way. An exclusion area never changes the size of an export. An exclusion area that hangs over the edge of a page cannot grow the exported page, and an exclusion area parented to the scene does not grow an exported scene unless you opt it into the export. ## Selection An exclusion area is the author's to move and resize. An adopter cannot select one, because a block created in the creator role carries no selection scope. Take the exclusion area out of reach in every role with: ```swift try engine.editor.setSelectionEnabled(exclusionArea, enabled: false) ``` ## Holding Content Out An exclusion area marks a region, and on its own it moves nothing. Set `exclusionArea/constrains` to `true` for a region the engine must hold content out of, such as an envelope window or a die cut: ```swift try engine.block.setBool(exclusion area, property: "exclusion area/constrains", value: true) ``` The exclusion area is then a wall while the user drags, nudges or resizes a block. The block stops against the exclusion area's own shape and slides along its edge, and the user drags around the exclusion area to reach the other side. An exclusion area shaped as a ring keeps blocks out of the ring and leaves the hole free, which is how a forbidden outer edge of a page is expressed. The exclusion area the block is up against draws a border on the canvas, so the user sees what stopped them. An exclusion area that marks without constraining draws that border too. The engine never moves a block on its own, so a block that already overlaps an exclusion area stays where it is, and a call through the API is never constrained. Ask which blocks overlap an exclusion area with: ```swift let offending = try engine.block.findAllInExclusionAreas() ``` ## Limitations Arrow keys are not constrained, and a block added through the API is placed wherever it is asked for. An exclusion area snaps by its bounding box while it constrains by its shape, so a curved exclusion area snaps as a rectangle. ## API Reference | Method | Purpose | | --- | --- | | `engine.block.create(.exclusionArea)` | Create an exclusion area. | | `engine.block.setShape(_:shape:)` | Give the exclusion area a non-rectangular outline. | | `engine.block.setFill(_:fill:)` | Put the artwork of the obstruction into the exclusion area. | | `engine.block.getFill(_:)` | Reach the stripe fill the exclusion area was created with. | | `engine.block.setIncludedInExport(_:enabled:)` | Put the exclusion area's artwork into an export. | | `engine.block.setBool(_:property:value:)` | Cut the exclusion area out of an export with `exclusionArea/punchOut`, or hold blocks out of it with `exclusionArea/constrains`. | | `engine.block.findAllInExclusionAreas()` | List the blocks that overlap a constraining exclusion area. | | `engine.editor.setSettingColor(_:r:g:b:a:)` | Set the wash and frame colors for every exclusion area. | | `engine.editor.setSelectionEnabled(_:enabled:)` | Take the exclusion area out of reach in every role. | ## Next Steps - [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) — page margins and the safety inset. - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — block types, shapes and fills. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Font Size Unit" description: "Configure how font sizes are interpreted (Point vs Pixel) per scene in the CE.SDK iOS engine." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/font-size-unit-3b2d60/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Font Size Unit](https://img.ly/docs/cesdk/mac-catalyst/concepts/font-size-unit-3b2d60/) --- ```swift file=@cesdk_swift_examples/engine-guides-font-size-unit/FontSizeUnit.swift reference-only import Foundation import IMGLYEngine @MainActor func fontSizeUnit(engine: Engine) async throws { // Create a default Pixel-based design scene. With designUnit `.px` and no // explicit fontSizeUnit, the engine pairs them and uses `.px` for fonts too. let scene = try engine.scene.create(designUnit: .px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1080) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) // Read the scene's current font-size unit. // For a Pixel-based scene this defaults to `.px`. let initialUnit = try engine.scene.getFontSizeUnit() print("Initial font-size unit:", initialUnit) // .px // Switch the scene-wide default to Point. Existing text keeps its visual // size; only the unit used by `setTextFontSize` and `getTextFontSizes` // changes. try engine.scene.setFontSizeUnit(.pt) print("After switch:", try engine.scene.getFontSizeUnit()) // .pt // Add a text block to demonstrate how the unit flows through the text APIs. let text = try engine.block.create(.text) try engine.block.appendChild(to: page, child: text) try engine.block.setString(text, property: "text/text", value: "Font Size Unit") try engine.block.setPositionX(text, value: 80) try engine.block.setPositionY(text, value: 480) try engine.block.setWidth(text, value: 920) try engine.block.setHeight(text, value: 120) // The value is interpreted in the scene's `fontSizeUnit`, which is now // Point. The engine reads this as 18 pt. try engine.block.setTextFontSize(text, fontSize: 18) // The float properties `text/fontSize`, `caption/fontSize`, and the // matching auto-min/max companions use the same `fontSizeUnit`. try engine.block.setFloat(text, property: "text/fontSize", value: 18) // The Swift binding does not expose a per-call unit option. To set a font // size in a different unit, switch the scene unit, perform the call, then // restore it. The engine converts using the scene's DPI so visual sizes // stay consistent. let savedUnit = try engine.scene.getFontSizeUnit() try engine.scene.setFontSizeUnit(.px) try engine.block.setTextFontSize(text, fontSize: 24) // interpreted as 24 px try engine.scene.setFontSizeUnit(savedUnit) // `getTextFontSizes` returns values in the scene's unit (currently Point). let sizesInSceneUnit = try engine.block.getTextFontSizes(text) print("Sizes (scene unit, pt):", sizesInSceneUnit) // `getFloat` reads `text/fontSize` in the same unit as `getTextFontSizes`. let propertySize = try engine.block.getFloat(text, property: "text/fontSize") print("text/fontSize (scene unit, pt):", propertySize) // To read in a different unit, switch the scene unit, read, then restore. try engine.scene.setFontSizeUnit(.px) let sizesInPixels = try engine.block.getTextFontSizes(text) try engine.scene.setFontSizeUnit(savedUnit) print("Sizes (px):", sizesInPixels) // When you create a scene yourself, you can pair both units explicitly. // If `fontSizeUnit` is omitted, the engine pairs it with `designUnit`: // `.px` design ⇒ `.px` font, `.mm` and `.in` ⇒ `.pt` font. _ = try engine.scene.create(designUnit: .px, fontSizeUnit: .pt) } ``` Pick the unit your scene uses for `setTextFontSize` and `getTextFontSizes`. The engine continues to store font sizes in points; this setting only changes how values are interpreted at the API boundary. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-font-size-unit) A scene's `fontSizeUnit` is the unit `setTextFontSize` and `getTextFontSizes` use to interpret values on text blocks. CE.SDK supports two values: `.pt` (the typographic default) and `.px` (matches Pixel-based design coordinates). The engine still stores font sizes in points internally; the unit only controls the API boundary. This guide covers reading and changing the scene's font-size unit, how that default flows through the text APIs, how to override the unit for a specific call, and how to pair the unit with the design unit at scene creation. ## Reading the Current Font-Size Unit Use `engine.scene.getFontSizeUnit()` to retrieve the font unit the current scene uses for the font size APIs. This sample creates the scene with `engine.scene.create(designUnit: .px)`, so the unit-aware overload pairs the font-size unit with the design unit and returns `.px`. When the unit-aware `engine.scene.create(designUnit:fontSizeUnit:sceneLayout:)` overload receives `fontSizeUnit: nil`, CE.SDK pairs the font-size unit with the design unit: `.px` uses `.px`, while `.mm` and `.in` use `.pt`. Loaded scenes saved before `fontSizeUnit` existed return `.pt` for compatibility. ```swift highlight-fontSizeUnit-getUnit // Read the scene's current font-size unit. // For a Pixel-based scene this defaults to `.px`. let initialUnit = try engine.scene.getFontSizeUnit() print("Initial font-size unit:", initialUnit) // .px ``` ## Setting the Font-Size Unit `engine.scene.setFontSizeUnit(_:)` switches the scene-wide default. Existing text retains its visual size — the engine still stores values in points and converts on the way in and out. Only subsequent `setTextFontSize` and `getTextFontSizes` calls use the new unit. ```swift highlight-fontSizeUnit-setUnit // Switch the scene-wide default to Point. Existing text keeps its visual // size; only the unit used by `setTextFontSize` and `getTextFontSizes` // changes. try engine.scene.setFontSizeUnit(.pt) print("After switch:", try engine.scene.getFontSizeUnit()) // .pt ``` `setDesignUnit(_:)` does not change `fontSizeUnit`, so a deliberate font-unit choice survives changes to the design coordinate system. ## Setting Font Sizes Without a Unit Option The Swift binding does not accept a unit option on `setTextFontSize`. Values passed in are always interpreted in the scene's `fontSizeUnit`. The same applies to the float properties `text/fontSize`, `caption/fontSize`, and the matching auto-min/max companions accessed through `setFloat(_:property:value:)` and `getFloat(_:property:)`. ```swift highlight-fontSizeUnit-implicitSet // The value is interpreted in the scene's `fontSizeUnit`, which is now // Point. The engine reads this as 18 pt. try engine.block.setTextFontSize(text, fontSize: 18) // The float properties `text/fontSize`, `caption/fontSize`, and the // matching auto-min/max companions use the same `fontSizeUnit`. try engine.block.setFloat(text, property: "text/fontSize", value: 18) ``` ## Overriding the Unit Per Call The Swift binding does not expose a per-call unit option. To set or read a font size in a different unit than the scene default, toggle the scene's `fontSizeUnit`, perform the call, and restore it. CE.SDK converts between units using the scene's DPI, so the text's visual size stays consistent. ```swift highlight-fontSizeUnit-overridePerCall // The Swift binding does not expose a per-call unit option. To set a font // size in a different unit, switch the scene unit, perform the call, then // restore it. The engine converts using the scene's DPI so visual sizes // stay consistent. let savedUnit = try engine.scene.getFontSizeUnit() try engine.scene.setFontSizeUnit(.px) try engine.block.setTextFontSize(text, fontSize: 24) // interpreted as 24 px try engine.scene.setFontSizeUnit(savedUnit) ``` ## Reading Font Sizes `getTextFontSizes` returns values in the scene's `fontSizeUnit`. Use the same toggle pattern when you need values in a different unit; the conversion applies on the way out, so the same text reads consistently in either unit without changing the underlying size. ```swift highlight-fontSizeUnit-readSizes // `getTextFontSizes` returns values in the scene's unit (currently Point). let sizesInSceneUnit = try engine.block.getTextFontSizes(text) print("Sizes (scene unit, pt):", sizesInSceneUnit) // `getFloat` reads `text/fontSize` in the same unit as `getTextFontSizes`. let propertySize = try engine.block.getFloat(text, property: "text/fontSize") print("text/fontSize (scene unit, pt):", propertySize) // To read in a different unit, switch the scene unit, read, then restore. try engine.scene.setFontSizeUnit(.px) let sizesInPixels = try engine.block.getTextFontSizes(text) try engine.scene.setFontSizeUnit(savedUnit) print("Sizes (px):", sizesInPixels) ``` ## Pairing Units at Scene Creation `engine.scene.create(designUnit:fontSizeUnit:sceneLayout:)` accepts both options. When `fontSizeUnit` is `nil`, CE.SDK pairs it with `designUnit` (`.px` ⇒ `.px`, `.mm` and `.in` ⇒ `.pt`). Pass both explicitly when you want to mix them — for example, a Pixel design with Point-based typography. ```swift highlight-fontSizeUnit-createWithUnits // When you create a scene yourself, you can pair both units explicitly. // If `fontSizeUnit` is omitted, the engine pairs it with `designUnit`: // `.px` design ⇒ `.px` font, `.mm` and `.in` ⇒ `.pt` font. _ = try engine.scene.create(designUnit: .px, fontSizeUnit: .pt) ``` Auto-pairing only applies to the unit-aware overload above. The layout-only `engine.scene.create(sceneLayout:)` overload creates a Pixel scene with `.pt`. `engine.scene.createVideo()`, `engine.scene.create(fromImage:dpi:pixelScaleFactor:sceneLayout:)`, and `engine.scene.create(fromVideo:)` also keep `.pt` for compatibility. Call `engine.scene.setFontSizeUnit(.px)` after creation if you want font sizes to match Pixel-based coordinates. ## The Angle Unit Angles have a per-scene unit too. `engine.scene.setAngleUnit(_:)` sets the unit in which angle properties such as `rotation`, `transform/skew` and `effect/linear_wipe/angle`, their keyframe values, and `setRotation(_:radians:)` and `getRotation(_:)` are interpreted. `.rad` is the default for every new scene and is not paired with `designUnit`. `engine.scene.getAngleUnit()` reads the unit, and the `angleUnit` parameter of `engine.scene.create(designUnit:fontSizeUnit:sceneLayout:angleUnit:)` sets it at creation. Like the font-size unit, it changes the values you pass and read, not the stored angles or the scene file. It also converts a number variable bound only to angle properties. CE.SDK uses radians for every angle property: `.deg` is a compatibility option for older integrations, and the CE.SDK editor UI expects radians. `motionBlur/shutterAngle`, `motionBlur/shutterPhase` and the `rotate` action take degrees in either unit. ```swift try engine.scene.setAngleUnit(.deg) ``` ## API Reference | Method | Purpose | | --- | --- | | `engine.scene.getFontSizeUnit()` | Get the current scene's font-size unit. | | `engine.scene.setFontSizeUnit(_:)` | Set the current scene's font-size unit. | | `engine.scene.create(designUnit:fontSizeUnit:sceneLayout:)` | Create a scene whose font-size unit is paired automatically with the design unit, or set explicitly. | | `engine.scene.create(sceneLayout:)` | Create a Pixel scene with the compatibility font-size unit `.pt`. | | `engine.scene.createVideo()` | Create a video scene with the compatibility font-size unit `.pt`. | | `engine.scene.create(fromImage:dpi:pixelScaleFactor:sceneLayout:)` | Create an image scene with the compatibility font-size unit `.pt`. | | `engine.scene.create(fromVideo:)` | Create a scene from a video URL with the compatibility font-size unit `.pt`. | | `engine.scene.getAngleUnit()` | Get the current scene's angle unit. | | `engine.scene.setAngleUnit(_:)` | Set the current scene's angle unit, `.rad` (default) or `.deg`. | | `engine.block.setTextFontSize(_:fontSize:in:)` | Set a text block's font size, or a text subrange, in the scene unit. | | `engine.block.getTextFontSizes(_:in:)` | Read a text block's font sizes, or a text subrange, in the scene unit. | | `engine.block.setFloat(_:property:value:)` / `engine.block.getFloat(_:property:)` with `text/fontSize` | Set or read the main text font-size property in the scene unit. | | `engine.block.setFloat(_:property:value:)` / `engine.block.getFloat(_:property:)` with `text/minAutomaticFontSize` | Set or read the text auto-resize minimum in the scene unit. | | `engine.block.setFloat(_:property:value:)` / `engine.block.getFloat(_:property:)` with `text/maxAutomaticFontSize` | Set or read the text auto-resize maximum in the scene unit. | | `engine.block.setFloat(_:property:value:)` / `engine.block.getFloat(_:property:)` with `caption/fontSize` | Set or read the caption font-size property in the scene unit. | | `engine.block.setFloat(_:property:value:)` / `engine.block.getFloat(_:property:)` with `caption/minAutomaticFontSize` | Set or read the caption auto-resize minimum in the scene unit. | | `engine.block.setFloat(_:property:value:)` / `engine.block.getFloat(_:property:)` with `caption/maxAutomaticFontSize` | Set or read the caption auto-resize maximum in the scene unit. | ## Next Steps - [Design Units](https://img.ly/docs/cesdk/mac-catalyst/concepts/design-units-cc6597/) — Configure pixels, millimeters, or inches and DPI for the scene's coordinate system. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Import and Export" description: "Understand every way files move in and out of CE.SDK—importing media and design files, loading and saving native scenes, and exporting finished output." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/import-export-0ebbd8/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Import and Export](https://img.ly/docs/cesdk/mac-catalyst/concepts/import-export-0ebbd8/) --- Understand every way files move in and out of CE.SDK—importing media and design files, loading and saving native scenes, and exporting finished output. When developers ask whether CE.SDK "supports" a file format, the answer depends on which workflow they mean. Importing an image as content, converting a Photoshop file into an editable design, and rendering a finished PDF are three different operations with different format support. This page maps every way files come in and go out, and links to the guide for each workflow. ![Diagram showing how files move in and out of CE.SDK: media assets, design files converted by importer packages and native design files flow into a scene; saving produces a re-editable scene string or archive, while exporting renders flattened final output](./assets/import-export-flows.svg) Everything in CE.SDK revolves around the *scene*—the block hierarchy that describes a design. Files coming in either add content to a scene or become a scene; files going out either preserve the scene for later editing or render it into final output. ## Three Ways Files Come In ### Media Assets Media assets are content placed *inside* a design: images, videos, audio clips and fonts. Users add them through the asset library, the device's photo library or the camera, and you add them programmatically through the asset and block APIs—to connect your own backend as a source, see [Create a Custom Importer](https://img.ly/docs/cesdk/mac-catalyst/import-media/create-custom-importer-0f7e16/). Media assets don't change the structure of a scene—they fill blocks with content. See the [Import Media](https://img.ly/docs/cesdk/mac-catalyst/import-media/overview-84bb23/) section for upload workflows and the asset library, [Asset Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) for the underlying architecture, and [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/import-media/file-format-support-8cdc84/) for the exact format list. ### Native Design Files CE.SDK persists designs in its own format, which loads back with full editability. Native files come in two variants: - **Scene string** (`.scene`) — A serialized description of the block hierarchy. Assets are referenced by URL, not embedded, so the file is lightweight but depends on those URLs staying reachable. - **Archive** (`.zip`) — A self-contained bundle of the scene file plus every referenced asset. Larger, but portable and usable offline. The `.imgly` extension is used for both variants; the engine detects the format automatically when loading. Load native files with `engine.scene.load()` or `engine.scene.loadArchive()`. See [Load a Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/), [Import a Design](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design-73b9c5/) and [Import from Archive](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-archive-dde9fa/). ### Design Files From Other Tools Files created in other design applications—Photoshop, InDesign, PowerPoint or PDF-producing tools—are not media assets and can't be added like an image. Instead, dedicated importer packages *convert* them into native CE.SDK scenes, preserving text, images, vector paths, positioning and colors as editable blocks: | Source format | Package | | -------------------- | ---------------------- | | Photoshop (`.psd`) | `@imgly/psd-importer` | | InDesign (`.idml`) | `@imgly/idml-importer` | | PDF (`.pdf`) | `@imgly/pdf-importer` | | PowerPoint (`.pptx`) | `@imgly/pptx-importer` | The importers are JavaScript packages and don't run inside the iOS SDK. The typical mobile workflow converts files on your server with Node.js, then loads the resulting scene or archive on the device. See [From InDesign](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-indesign-ba3988/) and [From Photoshop](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-photoshop-cca6bb/) for this workflow. #### Importer Features and Limitations Each importer translates a subset of its source format, so review converted scenes—especially complex documents—before publishing. Where a feature can't be mapped one-to-one, the importers degrade gracefully. Typical examples: - Linked, non-embedded images aren't resolved and import as placeholders (IDML, PDF). - Mixing multiple fonts or font sizes within a single Photoshop text layer isn't supported, and unavailable fonts are substituted depending on the configured font strategy. - PowerPoint tables, charts, SmartArt, animations and slide masters are skipped. The npm page of each package maintains the up-to-date list of supported features and limitations: [`@imgly/psd-importer`](https://www.npmjs.com/package/@imgly/psd-importer), [`@imgly/idml-importer`](https://www.npmjs.com/package/@imgly/idml-importer), [`@imgly/pdf-importer`](https://www.npmjs.com/package/@imgly/pdf-importer) and [`@imgly/pptx-importer`](https://www.npmjs.com/package/@imgly/pptx-importer). For any format without a ready-made importer, you can build your own converter with the scene and block APIs—see [Create From Scratch](https://img.ly/docs/cesdk/mac-catalyst/create-templates/from-scratch-663cda/) for building a design programmatically. ## Two Ways Files Go Out ### Saving a Design Saving preserves the scene for later editing. `engine.scene.saveToString()` produces a scene string with assets referenced by URL; `engine.scene.saveToArchive()` produces a self-contained archive with assets embedded. Both round-trip losslessly—loading a saved file restores the design exactly, with full editability. A saved design remains editable in CE.SDK. Saving an archive containing a page automatically generates a root `preview.png` that external tools can extract. Preview-generation failures omit the image. For scene strings or a different preview, export a thumbnail alongside the saved design. See [Save](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) and [Create Thumbnail](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/create-thumbnail-749be1/). ### Exporting Final Output Exporting renders the scene into a finished file for use outside CE.SDK. Export the whole scene, a single page or any individual block with `engine.block.export()`. Output covers images, vector graphics, print-ready PDF, video and audio, and raw pixel data—[File Format Support](https://img.ly/docs/cesdk/mac-catalyst/file-format-support-3c4b2a/) lists the exact formats on iOS and macOS. Each output has a focused guide: [To PNG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-png-f87eaf/), [To JPEG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-jpeg-6f88e9/), [To WebP](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-webp-aef6f4/), [To PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) (including [print-ready output](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-printing-bca896/) with underlayers and spot colors), [To MP4](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-mp4-c998a8/), [audio](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/audio-68de25/) and [raw RGBA buffers](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-raw-data-abd7da/). Exports are flattened: text becomes pixels or paths, and the block structure is gone. An exported file can't be turned back into the original editable design. See [Export Options](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) for configuration, plus [Partial Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/), [Pre-Export Validation](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/pre-export-validation-3a2cba/), [Export for Social Media](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-social-media-0e8a92/) and [Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/). ## Save vs. Export The most common point of confusion is the difference between saving and exporting: | | Save | Export | | ----------------------- | ---------------------------------------- | --------------------------------- | | Purpose | Continue editing later | Use the result outside CE.SDK | | Output | `.scene` string, `.imgly`/`.zip` archive | Rendered files (PNG, PDF, MP4, …) | | Re-editable | Yes, losslessly | No—content is flattened | | Assets | Referenced by URL or embedded | Rendered into the output | | Viewable outside CE.SDK | No | Yes | A typical integration uses both: save the scene to your backend so users can keep editing, and export when they download or publish the result. ## Common Questions - **Can users add a PDF or Photoshop file like an image?** Not as a media asset—the media path accepts images, video and audio only. PDF, PSD, IDML and PPTX files go through the importer packages, which convert them into editable scenes upstream of the mobile app. - **Why isn't my exported PDF editable when I import it back?** Exporting flattens the design. The PDF importer reconstructs an editable scene from any PDF, but it's a conversion, not a lossless round trip. To preserve editability, save the scene alongside the export. - **What's the difference between `.scene`, `.imgly` and `.zip`?** `.scene` is always a scene string, `.zip` is always an archive, and `.imgly` is the recommended extension for either—the engine detects which one it is when loading. - **Does a saved scene contain its images?** A scene string references assets by URL, so the design breaks if those URLs go away. An archive embeds all assets and is fully self-contained. ## Format Support Reference For the authoritative format tables—including codec details, size limits and known limitations—see [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/file-format-support-3c4b2a/). ## API Reference | Method | Category | Purpose | | ------------------------------ | -------- | ------------------------------------------------------------- | | `engine.scene.load()` | Import | Load a scene string, with automatic format detection | | `engine.scene.loadArchive()` | Import | Load a self-contained scene archive | | `engine.scene.saveToString()` | Save | Serialize the scene to a string with assets referenced by URL | | `engine.scene.saveToArchive()` | Save | Bundle the scene and all assets into a self-contained archive | | `engine.block.export()` | Export | Render a block, page or scene to a final output format | ## Next Steps - [Import Media](https://img.ly/docs/cesdk/mac-catalyst/import-media/overview-84bb23/) — Bring images, video, audio and fonts into designs - [Import a Design](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design-73b9c5/) — Load saved scenes and archives - [Import Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates/import-e50084/) — Load templates from URLs, archives and strings - [Save](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) — Persist designs for later editing - [Export Options](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Configure output formats and quality --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Pages" description: "Pages structure scenes in CE.SDK and must share the same dimensions to ensure consistent rendering." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Pages](https://img.ly/docs/cesdk/mac-catalyst/concepts/pages-7b6bae/) --- ```swift file=@cesdk_swift_examples/engine-guides-concepts-pages/Pages.swift reference-only import Foundation import IMGLYEngine @MainActor func pages(engine: Engine) async throws { // Create a scene with VerticalStack layout for multi-page designs let scene = try engine.scene.create(sceneLayout: .verticalStack) // Configure spacing between pages let stacks = try engine.block.find(byType: .stack) let stack = stacks[0] try engine.block.setFloat(stack, property: "stack/spacing", value: 20) try engine.block.setBool(stack, property: "stack/spacingInScreenspace", value: true) // Set page dimensions at the scene level (all pages share these dimensions) try engine.block.setFloat(scene, property: "scene/pageDimensions/width", value: 800) try engine.block.setFloat(scene, property: "scene/pageDimensions/height", value: 600) // Create the first page and set its dimensions let firstPage = try engine.block.create(.page) try engine.block.setWidth(firstPage, value: 800) try engine.block.setHeight(firstPage, value: 600) try engine.block.appendChild(to: stack, child: firstPage) // Create the second page with the same dimensions let secondPage = try engine.block.create(.page) try engine.block.setWidth(secondPage, value: 800) try engine.block.setHeight(secondPage, value: 600) try engine.block.appendChild(to: stack, child: secondPage) // Resolve sample assets against the bundled assets base URL. let baseURL = try engine.guidesBaseURL // Add an image block to the first page let imageBlock = try engine.block.create(.graphic) try engine.block.appendChild(to: firstPage, child: imageBlock) // Create a rect shape for the graphic block let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(imageBlock, shape: rectShape) // Configure size and position after appending to the page try engine.block.setWidth(imageBlock, value: 400) try engine.block.setHeight(imageBlock, value: 300) try engine.block.setPositionX(imageBlock, value: 200) try engine.block.setPositionY(imageBlock, value: 150) // Create and configure the image fill let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) // Add a text block to the second page let textBlock = try engine.block.create(.text) try engine.block.appendChild(to: secondPage, child: textBlock) // Configure text properties try engine.block.replaceText(textBlock, text: "Page 2") try engine.block.setTextFontSize(textBlock, fontSize: 48) try engine.block.setTextColor(textBlock, color: .rgba(r: 0.2, g: 0.2, b: 0.2, a: 1.0)) try engine.block.setEnum(textBlock, property: "text/horizontalAlignment", value: "Center") try engine.block.setWidthMode(textBlock, mode: .auto) try engine.block.setHeightMode(textBlock, mode: .auto) // Enable and set margins for print bleed on the first page try engine.block.setBool(firstPage, property: "page/marginEnabled", value: true) try engine.block.setFloat(firstPage, property: "page/margin/top", value: 10) try engine.block.setFloat(firstPage, property: "page/margin/bottom", value: 10) try engine.block.setFloat(firstPage, property: "page/margin/left", value: 10) try engine.block.setFloat(firstPage, property: "page/margin/right", value: 10) // Set custom title templates for each page try engine.block.setString(firstPage, property: "page/titleTemplate", value: "Cover") try engine.block.setString(secondPage, property: "page/titleTemplate", value: "Content") // Set a background fill on the second page let colorFill = try engine.block.createFill(.color) try engine.block.setColor(colorFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.95, b: 1.0, a: 1.0)) try engine.block.setFill(secondPage, fill: colorFill) // Get all pages in sorted order let allPages = try engine.scene.getPages() print("All pages:", allPages) print("Number of pages:", allPages.count) // Get the current page (nearest to viewport center or containing selection) let currentPage = try engine.scene.getCurrentPage() print("Current page:", currentPage as Any) // Find pages using the block API let pagesByType = try engine.block.find(byType: .page) print("Pages found by type:", pagesByType) // Enable single page mode and show only the second page try engine.editor.setSettingBool("features/singlePageModeEnabled", value: true) try engine.scene.setCurrentPages([secondPage]) // Or show more pages. Two pages form a facing pair try engine.scene.setCurrentPages([firstPage, secondPage]) // Read the shown pages. The array is empty while the mode is disabled let currentPages = try engine.scene.getCurrentPages() print("Current pages:", currentPages) // Turn the mode off to show every page again try engine.editor.setSettingBool("features/singlePageModeEnabled", value: false) } ``` Pages define the format of your designs — every graphic block, text element, and media file lives inside a page. This guide covers how pages fit into the scene hierarchy, their properties like margins and title templates, and how to configure page dimensions for different layout modes. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-concepts-pages) Pages provide the canvas and frame for your designs. Whether you're building a multi-page document, a social media carousel, or a video composition, understanding how pages work helps you structure content correctly. This guide covers: - Understanding the scene hierarchy: Scene → Pages → Blocks - Creating and managing multiple pages - Setting page dimensions at the scene level - Configuring page properties like margins and title templates - Navigating between pages programmatically ## Pages in the Scene Hierarchy In CE.SDK, content follows a strict hierarchy: a **scene** contains **pages**, and pages contain **content blocks**. Only blocks attached to a page are rendered on the canvas. ```swift highlight-pages-createScene // Create a scene with VerticalStack layout for multi-page designs let scene = try engine.scene.create(sceneLayout: .verticalStack) // Configure spacing between pages let stacks = try engine.block.find(byType: .stack) let stack = stacks[0] try engine.block.setFloat(stack, property: "stack/spacing", value: 20) try engine.block.setBool(stack, property: "stack/spacingInScreenspace", value: true) ``` When you create a scene with a layout mode like `verticalStack`, pages are automatically arranged according to that mode. Create pages using `engine.block.create(.page)`, set their dimensions with `setWidth()` and `setHeight()`, then attach them to the scene's stack container with `engine.block.appendChild(to:child:)`. ```swift highlight-pages-createPages // Create the first page and set its dimensions let firstPage = try engine.block.create(.page) try engine.block.setWidth(firstPage, value: 800) try engine.block.setHeight(firstPage, value: 600) try engine.block.appendChild(to: stack, child: firstPage) // Create the second page with the same dimensions let secondPage = try engine.block.create(.page) try engine.block.setWidth(secondPage, value: 800) try engine.block.setHeight(secondPage, value: 600) try engine.block.appendChild(to: stack, child: secondPage) ``` Content blocks must be added as children of a page to render. For graphic blocks, set both a shape and a fill for content to display. Append blocks to the page before configuring their properties. ```swift highlight-pages-addContent // Add an image block to the first page let imageBlock = try engine.block.create(.graphic) try engine.block.appendChild(to: firstPage, child: imageBlock) // Create a rect shape for the graphic block let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(imageBlock, shape: rectShape) // Configure size and position after appending to the page try engine.block.setWidth(imageBlock, value: 400) try engine.block.setHeight(imageBlock, value: 300) try engine.block.setPositionX(imageBlock, value: 200) try engine.block.setPositionY(imageBlock, value: 150) // Create and configure the image fill let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) // Add a text block to the second page let textBlock = try engine.block.create(.text) try engine.block.appendChild(to: secondPage, child: textBlock) // Configure text properties try engine.block.replaceText(textBlock, text: "Page 2") try engine.block.setTextFontSize(textBlock, fontSize: 48) try engine.block.setTextColor(textBlock, color: .rgba(r: 0.2, g: 0.2, b: 0.2, a: 1.0)) try engine.block.setEnum(textBlock, property: "text/horizontalAlignment", value: "Center") try engine.block.setWidthMode(textBlock, mode: .auto) try engine.block.setHeightMode(textBlock, mode: .auto) ``` ## Page Dimensions and Consistency The CE.SDK engine supports pages with different dimensions. When using stacked layout modes (`verticalStack`, `horizontalStack`), the Editor UI expects all pages to share the same size. With the `free` layout mode, you can set different dimensions for each page in the UI. ```swift highlight-pages-setDimensions // Set page dimensions at the scene level (all pages share these dimensions) try engine.block.setFloat(scene, property: "scene/pageDimensions/width", value: 800) try engine.block.setFloat(scene, property: "scene/pageDimensions/height", value: 600) ``` Set default page dimensions at the scene level using `engine.block.setFloat(_:property:value:)` with `scene/pageDimensions/width` and `scene/pageDimensions/height`. If you do not set them, they report the current size of the first page, or 1 when that page has no absolute width and height. The `scene/aspectRatioLock` property controls whether changing one dimension automatically adjusts the other. Individual pages can also have their dimensions set directly with `setWidth()` and `setHeight()`. ## Finding and Navigating Pages CE.SDK provides several methods to locate and navigate between pages in your scene. ```swift highlight-pages-findPages // Get all pages in sorted order let allPages = try engine.scene.getPages() print("All pages:", allPages) print("Number of pages:", allPages.count) // Get the current page (nearest to viewport center or containing selection) let currentPage = try engine.scene.getCurrentPage() print("Current page:", currentPage as Any) // Find pages using the block API let pagesByType = try engine.block.find(byType: .page) print("Pages found by type:", pagesByType) ``` Use these methods based on your needs: - `engine.scene.getPages()` returns all pages in sorted order - `engine.scene.getCurrentPage()` returns the page containing the current selection, or the page nearest to the viewport center - `engine.block.find(byType: .page)` finds all page blocks in the scene ## Page Properties Each page has its own properties that control its appearance and behavior. These are set on the page block itself, not on the scene. ### Margins Page margins define bleed areas useful for print designs. Enable margins and configure each side individually: ```swift highlight-pages-pageMargins // Enable and set margins for print bleed on the first page try engine.block.setBool(firstPage, property: "page/marginEnabled", value: true) try engine.block.setFloat(firstPage, property: "page/margin/top", value: 10) try engine.block.setFloat(firstPage, property: "page/margin/bottom", value: 10) try engine.block.setFloat(firstPage, property: "page/margin/left", value: 10) try engine.block.setFloat(firstPage, property: "page/margin/right", value: 10) ``` Set `page/marginEnabled` to `true` to enable margins, then use `page/margin/top`, `page/margin/bottom`, `page/margin/left`, and `page/margin/right` to configure each side. ### Safety Inset The safety inset is the inward inset from the page edge that content must stay inside. It is the counterpart of the bleed margin, which extends outward. Print shops cut with a tolerance, so text and logos placed too close to the edge can be trimmed off. Set `page/safetyEnabled` to `true`, then use `page/safetyInset/top`, `page/safetyInset/bottom`, `page/safetyInset/left`, and `page/safetyInset/right` to configure each side. Every inset defaults to `0`, so set a value as well as enabling the area. The safety inset is authoring state, and nothing it draws reaches an export. The guide appears while a drag is in the safety inset or comes near the safety line, and goes away when the drag ends. A move or resize with the arrow keys shows it the same way for 1.6 seconds, without snapping. The engine shades the band between the page edge and the line, draws the line itself, and lets the dragged block snap to it. Set `page/safetyRevealDuringTransform` to `false` to draw the guide on every page all the time instead. Set the guide colors with the `page/safetyFillColor` and `page/safetyFrameColor` settings; a fully transparent `page/safetyFrameColor` hides the line and turns its snapping off. ### Exclusion Area An exclusion area marks a region of a page that content must stay out of: a window on an envelope, a spine on a book cover, a glue flap on a box, an address panel on a mailer. Where the safety inset is one inset per page, a page can carry any number of exclusion areas, each with its own position, size and rotation. Create one with the `//ly.img.ubq/exclusionArea` block type and append it to a page. An exclusion area is a graphic, so it takes a shape, a fill, a stroke and the ordinary transform, and you can put the artwork of the obstruction into it. A new exclusion area is a rectangle with no fill and shows a striped pattern, so it is visible at once. Assign a fill to replace the stripes. An exclusion area is authoring state. The engine washes it in `page/exclusionAreaFillColor` and frames it in `page/exclusionAreaFrameColor`, neither of which reaches an export, and a fully transparent `page/exclusionAreaFrameColor` hides the frame. An exclusion area is left out of an export as well: set `includedInExport` to `true` on the exclusion area to put its artwork in the file. Set `exclusionArea/punchOut` to `true` to cut the exclusion area out of an export instead. The page and everything on it get a hole where the exclusion area is, so a die cut window in the design becomes a window in the file. It is off by default, and the canvas keeps showing the content either way. ### Title Template The `page/titleTemplate` property defines the display label shown for each page. It supports template variables like `{{ubq.page_index}}` for dynamic numbering. ```swift highlight-pages-titleTemplate // Set custom title templates for each page try engine.block.setString(firstPage, property: "page/titleTemplate", value: "Cover") try engine.block.setString(secondPage, property: "page/titleTemplate", value: "Content") ``` The default value is `"Page {{ubq.page_index}}"`. Customize this to show labels like "Slide 1", "Cover", or any custom text. ### Fill and Background Pages support fills for background colors or images using the standard fill system. ```swift highlight-pages-pageBackground // Set a background fill on the second page let colorFill = try engine.block.createFill(.color) try engine.block.setColor(colorFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.95, b: 1.0, a: 1.0)) try engine.block.setFill(secondPage, fill: colorFill) ``` Create a fill using `engine.block.createFill(.color)` or `engine.block.createFill(.image)`, configure its properties, then apply it to the page with `engine.block.setFill(_:fill:)`. ## Page Layout Modes The scene's layout mode controls how multiple pages are arranged. Set this when creating the scene with `engine.scene.create(sceneLayout:)` or update it with `engine.scene.setLayout()`: | Layout | Description | |--------|-------------| | `.verticalStack` | Pages stack vertically, one below the other (default for design) | | `.horizontalStack` | Pages arrange horizontally, side by side | | `.depthStack` | Pages overlay each other, typically used in video mode | | `.free` | Pages can be positioned freely without automatic arrangement | ## Single Page Mode Single page mode focuses the editor on one page (or a facing pair) of a multi-page design. All other pages leave the view but stay in the document and in the saved file. This mode is useful for print products like postcards, photo books, or apparel print areas. Enable the mode with the engine setting, then choose the shown page: ```swift highlight-pages-singlePageMode // Enable single page mode and show only the second page try engine.editor.setSettingBool("features/singlePageModeEnabled", value: true) try engine.scene.setCurrentPages([secondPage]) // Or show more pages. Two pages form a facing pair try engine.scene.setCurrentPages([firstPage, secondPage]) // Read the shown pages. The array is empty while the mode is disabled let currentPages = try engine.scene.getCurrentPages() print("Current pages:", currentPages) // Turn the mode off to show every page again try engine.editor.setSettingBool("features/singlePageModeEnabled", value: false) ``` The mode is view state only. `engine.scene.getPages()` still returns every page and a saved scene keeps every page. `engine.block.isVisible(_:)` still returns `true` for a page out of the view, and `engine.scene.getCurrentPages()` reports the view. An export of the scene holds every page, also the pages that the mode does not show. An export leaves out only pages that you hide with `engine.block.setVisible(_:visible:)`. ## Next Steps - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) — Learn about scene structure and management - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Understand the building blocks that live inside pages --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Working With Resources" description: "Preload resources, find transient data, detect MIME types, and relocate URLs in CE.SDK for Swift." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/resources-a58d71/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Resources](https://img.ly/docs/cesdk/mac-catalyst/concepts/resources-a58d71/) --- ```swift file=@cesdk_swift_examples/engine-guides-resources/Resources.swift reference-only import Foundation import IMGLYEngine @MainActor func resources(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) let baseURL = try engine.guidesBaseURL // Create a graphic block with an image fill. // The image loads on-demand when the engine renders the block. let imageBlock = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(imageBlock, shape: rectShape) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.setEnum(imageBlock, property: "contentFill/mode", value: "Cover") try engine.block.appendChild(to: page, child: imageBlock) // Preload all resources in the scene before rendering. try await engine.block.forceLoadResources([scene]) // Preload specific blocks only. let graphics = try engine.block.find(byType: .graphic) try await engine.block.forceLoadResources(graphics) // Create a video fill and preload its resource to query properties. let videoBlock = try engine.block.create(.graphic) let videoShape = try engine.block.createShape(.rect) try engine.block.setShape(videoBlock, shape: videoShape) let videoFill = try engine.block.createFill(.video) try engine.block.setURL( videoFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) try engine.block.setFill(videoBlock, fill: videoFill) try engine.block.setEnum(videoBlock, property: "contentFill/mode", value: "Cover") try engine.block.appendChild(to: page, child: videoBlock) try await engine.block.forceLoadAVResource(videoFill) let duration = try engine.block.getAVResourceTotalDuration(videoFill) let videoWidth = try engine.block.getVideoWidth(videoFill) let videoHeight = try engine.block.getVideoHeight(videoFill) print("Video: \(duration)s, \(videoWidth)x\(videoHeight)") // Find transient resources that won't survive serialization. let transientResources = try engine.editor.findAllTransientResources() for resource in transientResources { print("Transient: \(resource.url), \(resource.size) bytes") } // Get all media URIs referenced in the scene. let mediaURIs = try engine.editor.findAllMediaURIs() for uri in mediaURIs { print("Media URI: \(uri)") } // List blocks that are not attached to any scene and free their memory before saving. let unusedBlocks = engine.block.findAllUnused() for blockID in unusedBlocks { try engine.block.destroy(blockID) } print("Destroyed \(unusedBlocks.count) unused blocks") // Detect the MIME type of a resource. let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg") let mimeType = try await engine.editor.getMIMEType(url: imageURL) print("MIME type: \(mimeType)") // Update a resource's URL mapping after moving it to a new location. let currentURL = URL(string: "https://example.com/old-location/image.jpg")! let relocatedURL = URL(string: "https://cdn.example.com/new-location/image.jpg")! try engine.editor.relocateResource(currentURL: currentURL, relocatedURL: relocatedURL) // Save the scene with a persistence callback for transient resources. let sceneString = try await engine.scene.saveToString( allowedResourceSchemes: ["http", "https"], onDisallowedResourceScheme: { url, _ in // Upload the resource to permanent storage and return the new URL. // let permanentURL = try await uploadToCDN(url) // return permanentURL url }, ) print("Saved scene (\(sceneString.count) characters)") } ``` Manage external media files—images, videos, audio, and fonts—that blocks reference via URIs in CE.SDK. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-resources) Resources are external media files that blocks reference through URI properties like `fill/image/imageFileURI` or `fill/video/fileURI`. CE.SDK loads resources automatically when needed, but you can preload them for better performance. When working with temporary data like buffers or blobs, you need to persist them before saving. If resource URLs change (such as during CDN migration), you can update the mappings without modifying scene data. This guide covers on-demand and preloaded resource loading, identifying and persisting transient resources, relocating resources when URLs change, and discovering all media URIs in a scene. | Method | Category | Purpose | | --- | --- | --- | | `engine.block.forceLoadResources(_:)` | Preloading | Load resources for blocks and their children | | `engine.block.forceLoadAVResource(_:)` | Preloading | Load audio/video resource for a block | | `engine.block.getAVResourceTotalDuration(_:)` | Properties | Get total duration of audio/video resource | | `engine.block.getVideoWidth(_:)` | Properties | Get video resource width in pixels | | `engine.block.getVideoHeight(_:)` | Properties | Get video resource height in pixels | | `engine.editor.findAllTransientResources()` | Discovery | Find temporary resources that need persistence | | `engine.editor.findAllMediaURIs()` | Discovery | Get all media URIs referenced in the scene | | `engine.block.findAllUnused()` | Discovery | Get all blocks that are not attached to any scene | | `engine.editor.getMIMEType(url:)` | Discovery | Get the MIME type of a resource | | `engine.editor.relocateResource(currentURL:relocatedURL:)` | Management | Update URL mapping for a relocated resource | | `engine.scene.saveToString(allowedResourceSchemes:onDisallowedResourceScheme:)` | Serialization | Save scene with resource scheme handling | ## On-Demand Loading The engine fetches resources automatically when rendering blocks or preparing exports. This approach requires no extra code but may delay the initial render while resources download. ```swift highlight-resources-onDemandLoading // Create a graphic block with an image fill. // The image loads on-demand when the engine renders the block. let imageBlock = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(imageBlock, shape: rectShape) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.setEnum(imageBlock, property: "contentFill/mode", value: "Cover") try engine.block.appendChild(to: page, child: imageBlock) ``` When you create a block with an image fill, the image doesn't load immediately. The engine fetches it when the block first renders on the canvas. ## Preloading Resources Load resources before they're needed with `forceLoadResources(_:)`. Pass block IDs to load resources for those blocks and their children. Preloading eliminates render delays and is useful when you want the scene fully ready before displaying it. ```swift highlight-resources-preloadResources // Preload all resources in the scene before rendering. try await engine.block.forceLoadResources([scene]) // Preload specific blocks only. let graphics = try engine.block.find(byType: .graphic) try await engine.block.forceLoadResources(graphics) ``` Pass the scene to preload all resources in the entire design, or pass specific blocks to load only what you need. Pass an empty array to load every resource currently known to the engine. ## Preloading Audio and Video Audio and video resources require `forceLoadAVResource(_:)` for full metadata access. The engine needs to download and parse media files before you can query properties like duration or dimensions. ```swift highlight-resources-preloadAV // Create a video fill and preload its resource to query properties. let videoBlock = try engine.block.create(.graphic) let videoShape = try engine.block.createShape(.rect) try engine.block.setShape(videoBlock, shape: videoShape) let videoFill = try engine.block.createFill(.video) try engine.block.setURL( videoFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) try engine.block.setFill(videoBlock, fill: videoFill) try engine.block.setEnum(videoBlock, property: "contentFill/mode", value: "Cover") try engine.block.appendChild(to: page, child: videoBlock) try await engine.block.forceLoadAVResource(videoFill) let duration = try engine.block.getAVResourceTotalDuration(videoFill) let videoWidth = try engine.block.getVideoWidth(videoFill) let videoHeight = try engine.block.getVideoHeight(videoFill) print("Video: \(duration)s, \(videoWidth)x\(videoHeight)") ``` Without preloading, properties like `getAVResourceTotalDuration(_:)` or `getVideoWidth(_:)` may return zero or incomplete values. ## Finding Transient Resources Transient resources are temporary data stored in buffers or blobs that won't survive scene serialization. Use `findAllTransientResources()` to discover them before saving. ```swift highlight-resources-findTransient // Find transient resources that won't survive serialization. let transientResources = try engine.editor.findAllTransientResources() for resource in transientResources { print("Transient: \(resource.url), \(resource.size) bytes") } ``` Each entry includes the resource URL and its size in bytes. Common transient resources include images from clipboard paste operations, camera captures, or programmatically generated content. ## Finding Media URIs Get all media file URIs referenced in a scene with `findAllMediaURIs()`. This returns a deduplicated list of URLs from image fills, video fills, audio blocks, and other media sources. ```swift highlight-resources-findMediaURIs // Get all media URIs referenced in the scene. let mediaURIs = try engine.editor.findAllMediaURIs() for uri in mediaURIs { print("Media URI: \(uri)") } ``` Use this for pre-fetching resources, validating availability, or building a manifest of all assets in a design. ## Finding Unused Blocks List every block that is not attached to any scene with `findAllUnused()`. A block is considered unused when it has no scene reference and no ancestor that belongs to a scene. Render blocks (fills, effects, shapes, blurs) are excluded. ```swift highlight-resources-findUnusedBlocks // List blocks that are not attached to any scene and free their memory before saving. let unusedBlocks = engine.block.findAllUnused() for blockID in unusedBlocks { try engine.block.destroy(blockID) } print("Destroyed \(unusedBlocks.count) unused blocks") ``` Pair this with `findAllMediaURIs()` to skip relocating resources for blocks that are no longer reachable, or call `engine.block.destroy(_:)` on each id to free memory before saving. ## Detecting MIME Types Determine a resource's content type with `getMIMEType(url:)`. The engine downloads the resource if it's not already cached. ```swift highlight-resources-detectMIMEType // Detect the MIME type of a resource. let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg") let mimeType = try await engine.editor.getMIMEType(url: imageURL) print("MIME type: \(mimeType)") ``` Common return values include `image/jpeg`, `image/png`, `video/mp4`, and `audio/mpeg`. This is useful when you need to verify resource types or make format-dependent decisions. ## Relocating Resources Update URL mappings when resources move with `relocateResource(currentURL:relocatedURL:)`. This updates all resource references in the scene and clears the internal cache. ```swift highlight-resources-relocate // Update a resource's URL mapping after moving it to a new location. let currentURL = URL(string: "https://example.com/old-location/image.jpg")! let relocatedURL = URL(string: "https://cdn.example.com/new-location/image.jpg")! try engine.editor.relocateResource(currentURL: currentURL, relocatedURL: relocatedURL) ``` Use relocation after uploading resources to a CDN or when migrating assets between storage locations. The engine updates all references in the scene and clears cached data so that the resource is fetched from the new URL. ## Persisting Transient Resources Handle transient resources during save with the `onDisallowedResourceScheme` callback in `saveToString`. The callback receives each resource URL with a disallowed scheme (like `buffer:` or `blob:`) and returns the permanent URL after uploading. ```swift highlight-resources-persistTransient // Save the scene with a persistence callback for transient resources. let sceneString = try await engine.scene.saveToString( allowedResourceSchemes: ["http", "https"], onDisallowedResourceScheme: { url, _ in // Upload the resource to permanent storage and return the new URL. // let permanentURL = try await uploadToCDN(url) // return permanentURL url }, ) print("Saved scene (\(sceneString.count) characters)") ``` This pattern lets you intercept temporary resources, upload them to permanent storage, and save the scene with stable URLs that will work when reloaded. ## Troubleshooting **Slow initial render**: Preload resources with `forceLoadResources(_:)` before displaying the scene. **Export fails with missing resources**: Check `findAllTransientResources()` and persist any temporary resources before export. **Video duration returns 0**: Ensure the video resource is loaded with `forceLoadAVResource(_:)` before querying properties. **Resources not found after reload**: Transient resources (buffers, blobs) are not serialized—relocate them to persistent URLs before saving. ## Next Steps - [Buffers](https://img.ly/docs/cesdk/mac-catalyst/concepts/buffers-9c565b/) — Work with in-memory data - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) — Understand scene serialization and persistence - [Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) — Learn about exporting designs --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Scenes" description: "Create, configure, save, and load scenes—the root container for all design elements in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) --- ```swift file=@cesdk_swift_examples/engine-guides-modifying-scenes/ModifyingScenes.swift reference-only import Foundation import IMGLYEngine @MainActor func modifyingScenes(engine: Engine) async throws { let scene = try engine.scene.create(sceneLayout: .verticalStack) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let block = try engine.block.create(.graphic) let shape = try engine.block.createShape(.rect) try engine.block.setShape(block, shape: shape) let fill = try engine.block.createFill(.color) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: 200) try engine.block.setHeight(block, value: 200) try engine.block.appendChild(to: page, child: block) let designUnit = try engine.scene.getDesignUnit() print("Design unit: \(designUnit)") try engine.scene.setDesignUnit(.mm) let layout = try engine.scene.getLayout() print("Layout: \(layout)") // The preview motion blur and video exports without a frame rate use this rate try engine.block.setFloat(scene, property: "scene/frameRate", value: 25) // Markers name points or spans on the scene timeline try engine.scene.addMarker(name: "intro", time: 0, duration: 2.5) try engine.scene.addMarker(name: "beat", time: 4) try engine.scene.removeMarker(at: 1) print("Markers:", try engine.scene.getMarkers()) // [SceneMarker(name: "intro", time: 0.0, duration: 2.5)] let pages = try engine.scene.getPages() print("Number of pages: \(pages.count)") let currentPage = try engine.scene.getCurrentPage() print("Current page: \(String(describing: currentPage))") try await engine.scene.zoom(to: page, paddingLeft: 20, paddingTop: 20, paddingRight: 20, paddingBottom: 20) let zoomLevel = try engine.scene.getZoom() print("Zoom level: \(zoomLevel)") try engine.scene.setZoom(1.0) let savedScene = try await engine.scene.saveToString() print("Scene saved, length: \(savedScene.count)") let loadedScene = try await engine.scene.load(from: savedScene) print("Scene loaded: \(loadedScene)") let zoomTask = Task { for await _ in engine.scene.onZoomLevelChanged { let zoom = try engine.scene.getZoom() print("Zoom changed: \(zoom)") } } let activeTask = Task { for await _ in engine.scene.onActiveChanged { print("Active scene changed") } } zoomTask.cancel() activeTask.cancel() } ``` Scenes are the root container for all designs in CE.SDK. They hold pages, blocks, and the camera that controls what you see in the canvas—and the engine manages only one active scene at a time. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-modifying-scenes) Every design you create starts with a scene. Scenes contain pages, and pages contain the visible design elements—text, images, shapes, and other blocks. Understanding how scenes work is essential for building, saving, and restoring user designs. This guide covers how to create scenes from scratch, manage pages within scenes, configure scene properties, save and load designs, and control the camera's zoom and position. ## Scene Hierarchy Scenes form the root of CE.SDK's design structure. The hierarchy works as follows: - **Scene** — The root container holding all design content - **Pages** — Direct children of scenes, arranged according to the scene's layout - **Blocks** — Design elements (text, images, shapes) that belong to pages Only blocks attached to pages within the active scene are rendered in the canvas. Use `engine.scene.get()` to retrieve the current scene and `engine.scene.getPages()` to access its pages. ## Creating Scenes ### Creating an Empty Scene Use `engine.scene.create(sceneLayout:)` to create a new design scene with a configurable page layout. The `sceneLayout` parameter controls how pages are arranged in the canvas. ```swift highlight-create-scene let scene = try engine.scene.create(sceneLayout: .verticalStack) ``` Available layouts: | Layout | Description | |--------|-------------| | `.verticalStack` | Pages arranged vertically | | `.horizontalStack` | Pages arranged horizontally | | `.depthStack` | Pages layered on top of each other | | `.free` | Manual positioning (default) | ### Creating for Video Editing For video projects, use `engine.scene.createVideo()` which configures the scene for timeline-based editing. Unlike `create(sceneLayout:)`, this method takes no parameters — page dimensions are set separately after creation. ### Creating from Media Files Create scenes directly from images or videos using `engine.scene.create(fromImage:)` and `engine.scene.create(fromVideo:)`. The scene dimensions match the source media. ### Adding Pages After creating a scene, add pages using `engine.block.create(.page)`. Configure the page dimensions and append it to the scene. ```swift highlight-create-page let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) ``` ### Adding Blocks With pages in place, add design elements like shapes, text, or images. Create a graphic block, configure its shape and fill, then append it to a page. ```swift highlight-create-block let block = try engine.block.create(.graphic) let shape = try engine.block.createShape(.rect) try engine.block.setShape(block, shape: shape) let fill = try engine.block.createFill(.color) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: 200) try engine.block.setHeight(block, value: 200) try engine.block.appendChild(to: page, child: block) ``` ## Scene Properties ### Design Units Query or configure how measurements are interpreted using `engine.scene.getDesignUnit()` and `engine.scene.setDesignUnit()`. This is useful for print workflows where precise physical dimensions matter. ```swift highlight-scene-properties let designUnit = try engine.scene.getDesignUnit() print("Design unit: \(designUnit)") try engine.scene.setDesignUnit(.mm) let layout = try engine.scene.getLayout() print("Layout: \(layout)") ``` Supported units are `.px`, `.mm`, and `.in`. ### Scene Layout Control how pages are arranged using `engine.scene.getLayout()` and `engine.scene.setLayout()`. The layout affects how users navigate between pages in multi-page designs. ### Frame Rate and Markers > **Caution:** Experimental: these APIs may change in a minor release. The `scene/frameRate` property sets the frames per second of the scene, and markers name points or spans on its timeline. For the range of the frame rate, reading, finding and removing markers, and the errors to expect, see [Markers and Frame Rate](https://img.ly/docs/cesdk/mac-catalyst/create-video/markers-and-frame-rate-517cc2/). ```swift highlight-frame-rate-and-markers // The preview motion blur and video exports without a frame rate use this rate try engine.block.setFloat(scene, property: "scene/frameRate", value: 25) // Markers name points or spans on the scene timeline try engine.scene.addMarker(name: "intro", time: 0, duration: 2.5) try engine.scene.addMarker(name: "beat", time: 4) try engine.scene.removeMarker(at: 1) print("Markers:", try engine.scene.getMarkers()) // [SceneMarker(name: "intro", time: 0.0, duration: 2.5)] ``` ## Page Navigation Access pages within your scene using these methods: ```swift highlight-page-navigation let pages = try engine.scene.getPages() print("Number of pages: \(pages.count)") let currentPage = try engine.scene.getCurrentPage() print("Current page: \(String(describing: currentPage))") ``` `getCurrentPage()` returns the page nearest to the viewport center—useful for determining which page the user is currently viewing. For more advanced block queries, use `engine.scene.findNearestToViewPortCenter(byType:)` and `engine.scene.findNearestToViewPortCenter(byKind:)`. ## Camera and Zoom ### Zoom to Block Use `engine.scene.zoom(to:)` to frame a specific block in the viewport with padding. Pass the scene block to show all pages. ```swift highlight-camera-zoom try await engine.scene.zoom(to: page, paddingLeft: 20, paddingTop: 20, paddingRight: 20, paddingBottom: 20) let zoomLevel = try engine.scene.getZoom() print("Zoom level: \(zoomLevel)") try engine.scene.setZoom(1.0) ``` ### Zoom Level Get and set the zoom level directly with `engine.scene.getZoom()` and `engine.scene.setZoom()`. A zoom level of `1.0` means one design unit equals one screen pixel. ### Auto-Fit Zoom For continuous auto-framing, use `engine.scene.enableZoomAutoFit()` to automatically keep a block centered as the viewport resizes. Disable it with `engine.scene.disableZoomAutoFit()` and check the current state with `engine.scene.isZoomAutoFitEnabled()`. ## Saving Scenes ### Saving to String Use `engine.scene.saveToString()` to serialize the current scene. This captures the complete scene structure—pages, blocks, and their properties—as a string you can store. ```swift highlight-save-scene let savedScene = try await engine.scene.saveToString() print("Scene saved, length: \(savedScene.count)") ``` The serialized string references external assets by URL rather than embedding them. For complete portability including assets, use `engine.scene.saveToArchive()`. ## Loading Scenes ### Loading from String Use `engine.scene.load(from:)` to restore a scene from a saved string: ```swift highlight-load-scene let loadedScene = try await engine.scene.load(from: savedScene) print("Scene loaded: \(loadedScene)") ``` Loading a new scene replaces any existing scene. The engine only holds one active scene at a time. ### Loading from URL Use `engine.scene.load(from:)` with a `URL` to load a scene directly from a local/remote location. The same call also loads archives that bundle all referenced assets — the engine detects the file kind automatically. Both scenes and archives use the `.imgly` extension; `.scene` and `.zip` files also load. ### Applying Templates Apply template content to the current scene using `engine.scene.applyTemplate(from:)`, which accepts either a `URL` or a `String`. Template content is automatically scaled to fit the current page dimensions. ## Event Subscriptions Subscribe to scene-related events using Swift's `AsyncStream` to react to changes in real time. ```swift highlight-event-subscriptions let zoomTask = Task { for await _ in engine.scene.onZoomLevelChanged { let zoom = try engine.scene.getZoom() print("Zoom changed: \(zoom)") } } let activeTask = Task { for await _ in engine.scene.onActiveChanged { print("Active scene changed") } } zoomTask.cancel() activeTask.cancel() ``` | Event | Description | |-------|-------------| | `onZoomLevelChanged` | Fires when the zoom level changes | | `onActiveChanged` | Fires when the active scene changes | ## Next Steps - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Create and manipulate design elements within pages --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Templating" description: "Understand how templates work in CE.SDK—reusable designs with variables for dynamic text and placeholders for swappable media." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/templating-f94385/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Templating](https://img.ly/docs/cesdk/mac-catalyst/concepts/templating-f94385/) --- Templates transform static designs into dynamic, data-driven content. They combine reusable layouts with variable text and placeholder media, enabling personalization at scale. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-concepts-templating) A template is a regular CE.SDK scene that contains **variable tokens** in text and **placeholder blocks** for media. When you load a template, you can populate the variables with data and swap placeholder content—producing personalized designs without modifying the underlying layout. ```swift file=@cesdk_swift_examples/engine-guides-concepts-templating/Templating.swift reference-only import Foundation import IMGLYEngine @MainActor func templating(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let templateURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: templateURL) let variableNames = engine.variable.findAll() print("Template variables:", variableNames) try engine.variable.set(key: "Name", value: "Jane") try engine.variable.set(key: "Greeting", value: "Wish you were here!") let placeholders = engine.block.findAllPlaceholders() print("Template placeholders:", placeholders.count) } ``` This guide explains the core concepts. For implementation details, see the guides linked in each section. ## What Makes a Template Any CE.SDK scene can become a template by adding dynamic elements: | Element | Purpose | Example | |---------|---------|---------| | **Variables** | Dynamic text replacement | `Hello, {{firstName}}!` | | **Placeholders** | Swappable media slots | Profile photo, product image | | **Editing Constraints** | Protected design elements | Locked logo, fixed layout | Templates separate **design** (created once by designers) from **content** (populated at runtime with data). This enables workflows like batch generation, form-based customization, and user personalization. ## Loading Templates Load a template from a URL using `engine.scene.load(from:)`. This loads the template's structure into the engine, including any pages, blocks, variables, and placeholders. ```swift highlight-templating-loadTemplate let templateURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: templateURL) ``` Templates are standard CE.SDK scene files. You can load them from your own servers, CDNs, or cloud storage. ## Variables Variables enable dynamic text without modifying the design structure. Text blocks contain `{{variableName}}` tokens that CE.SDK resolves at render time. ### Discovering Variables Use `engine.variable.findAll()` to discover what variables a template expects: ```swift highlight-templating-discoverVariables let variableNames = engine.variable.findAll() print("Template variables:", variableNames) ``` This returns an array of variable names defined in the template. ### Setting Variable Values Populate variables with `engine.variable.set(key:value:)`: ```swift highlight-templating-setVariables try engine.variable.set(key: "Name", value: "Jane") try engine.variable.set(key: "Greeting", value: "Wish you were here!") ``` **How variables work:** - Reference them in text blocks: `Welcome, {{name}}!` - CE.SDK automatically updates all text blocks using that variable - Tokens are case-sensitive; unmatched tokens render as literal text - Variables are scene-scoped and persist when you save the template [Learn more about text variables →](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) ## Placeholders Placeholders mark blocks as content slots that users or automation can replace. When you enable placeholder behavior on an image block, it becomes a designated swap target. ### Discovering Placeholders Use `engine.block.findAllPlaceholders()` to discover all placeholder blocks in a loaded template: ```swift highlight-templating-discoverPlaceholders let placeholders = engine.block.findAllPlaceholders() print("Template placeholders:", placeholders.count) ``` **How placeholders work:** - Enable with `engine.block.setPlaceholderEnabled(_:enabled:)` - Placeholder blocks are marked for content replacement - You can programmatically replace placeholder content with new images or media [Learn more about placeholders →](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) ## Template Workflows Templates support several common workflows: ### Batch Generation Load a template programmatically, iterate through data records, set variables for each record, and export personalized designs. This powers use cases like certificates, badges, and personalized marketing. ### Form-Based Customization Load a template, present a form for variable values, and let users customize text while the design stays consistent. The editor UI handles placeholder replacement through drag-and-drop. ### Design Systems Create template libraries where designers maintain approved layouts and automation populates them with data at scale. ## Creating Templates Build templates by adding variable tokens to text blocks and configuring placeholder behavior on media blocks. Save with `engine.scene.saveToString()` or `engine.scene.saveToArchive()`. [Learn more about creating templates →](https://img.ly/docs/cesdk/mac-catalyst/create-templates/from-scratch-663cda/) ## Importing Templates CE.SDK provides two approaches for working with templates: **Load a template** with `engine.scene.load(from:)` to replace the current scene entirely, including page dimensions. **Apply a template** with `engine.scene.applyTemplate(from:)` to merge template content into an existing scene while preserving current page dimensions. ## Next Steps - [Create Templates From Scratch](https://img.ly/docs/cesdk/mac-catalyst/create-templates/from-scratch-663cda/) — Build a reusable template with variables and placeholders. - [Import Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates/import-e50084/) — Load and import design templates into CE.SDK from URLs, archives, and serialized strings. - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) — Reference variables inside text blocks and update them at runtime. - [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) — Mark blocks as swap targets for template consumers. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Terminology" description: "Definitions for the core terms and concepts used throughout CE.SDK documentation, including Engine, Scene, Block, Fill, Shape, Effect, and more." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/terminology-99e82d/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Terminology](https://img.ly/docs/cesdk/mac-catalyst/concepts/terminology-99e82d/) --- A reference guide to the core terms and concepts used throughout CE.SDK documentation. CE.SDK uses consistent terminology across all platforms. Understanding what we call things helps you navigate the API, read documentation efficiently, and communicate effectively with other developers working on CE.SDK integration. ## Core Architecture ### Engine All operations—creating scenes, manipulating blocks, rendering, and exporting—go through the *Engine*. In Swift, this is the `Engine` class from `IMGLYEngine`. Initialize it once and use it throughout your application's lifecycle. ### Scene The root container for all design content. A *Scene* contains *Pages*, which contain *Blocks*. Only one *Scene* can be active per *Engine* instance. You can create a *Scene* programmatically or load one from a file. *Scenes* operate in one of two modes: - **Design Mode**: Static designs like social posts, print materials, and graphics - **Video Mode**: Timeline-based content with duration, playback, and animation See [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) for details. ### Page *Pages* are containers within a *Scene* that hold content *Blocks* (see below) and define working area dimensions. In *Design Mode*, pages are individual artboards. In *Video Mode*, pages are timeline compositions where *Blocks* are arranged across time. ### Block The fundamental building unit in CE.SDK. Everything visible in a design is a *Block*—images, text, shapes, graphics, audio, video—and even *Pages* themselves. *Blocks* form a parent-child hierarchy. Each *Block* has two identifiers: - **DesignBlockID**: A numeric handle (integer) used in API calls - **UUID**: A stable string identifier that persists across save and load operations See [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) for details. ## Block Anatomy Modify a *Block's* appearance and behavior by attaching *Fills*, *Shapes*, and *Effects*. Most of these modifiers must be created separately and then attached to a *Block*. ### Fill *Fills* cover the surface of a *Block's* shape: - **Color Fill**: Solid color - **Gradient Fill**: Linear, radial, or conical gradients - **Image Fill**: Image content - **Video Fill**: Video content See the Color Fills, Gradient Fills, Image Fills, and Video Fills guides. ### Shape *Shapes* define a *Block's* outline and dimensions, determining the silhouette and how the *Fill* is clipped. *Shape* types include: - **Rect**: Rectangles and squares - **Ellipse**: Circles and ovals - **Polygon**: Multi-sided shapes - **Star**: Star shapes with configurable points - **Line**: Straight lines - **Vector Path**: Custom vector shapes Like *Fills*, *Shapes* are created separately and attached to *Blocks*. ### Effect *Effects* are non-destructive visual modifications applied to a *Block*. Multiple *Effects* can be stacked. *Effect* categories include: - **Adjustments**: Brightness, contrast, saturation, and other image corrections - **Filters**: LUT-based color grading, duotone - **Stylization**: Pixelize, posterize, half-tone, dot pattern, linocut, outliner - **Distortion**: Liquid, mirror, shifter, cross-cut, extrude blur - **Focus**: Tilt-shift, vignette - **Color**: Recolor, green screen (chroma key) - **Other**: Glow, TV glitch The order determines how multiple effects attached to a single block interact. ### Blur A modifier that reduces sharpness. *Blur* types include: - **Uniform Blur**: Even blur across the entire block - **Radial Blur**: Circular blur from a center point - **Mirrored Blur**: Blur with reflection > **Note:** **Blur has a dedicated API because it composites differently than other effects.** While most effects like brightness or saturation operate only on a block's own pixels, blur needs to sample pixels from the surrounding area to calculate the blurred result. This means blur interacts with the scene's layering and transparency in ways other effects don't—when you blur a partially transparent block, the engine must handle how that blur blends with whatever content sits behind it. See [Blur](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/blur-71d642/) for details. ### Drop Shadow A built-in block property (not an *Effect*) that renders a shadow beneath blocks. *Drop Shadow* has dedicated API methods for enabling, color, offset, and blur radius. > **Warning:** Unlike effects, drop shadow is configured directly on the block rather than created and attached separately. ## Block Handling These terms describe how *Blocks* are categorized and identified. ### Type The built-in *Type* defines a *Block's* core behavior and available properties. *Type* is immutable—you choose it when creating the *Block*. - `//ly.img.ubq/graphic` — Visual block for images, shapes, and graphics - `//ly.img.ubq/text` — Text content - `//ly.img.ubq/audio` — Audio content - `//ly.img.ubq/page` — Page container - `//ly.img.ubq/scene` — Root scene container - `//ly.img.ubq/track` — Video timeline track - `//ly.img.ubq/stack` — Stack container for layering - `//ly.img.ubq/group` — Group container for organizing blocks - `//ly.img.ubq/camera` — Camera for scene viewing - `//ly.img.ubq/cutout` — Cutout/mask block - `//ly.img.ubq/exclusionArea` — An editor-only region content must stay out of - `//ly.img.ubq/caption` — Caption/subtitle block - `//ly.img.ubq/captionTrack` — Track for captions The *Type* determines which properties and capabilities a *Block* has. ### Kind A custom string label you assign to categorize *Blocks* for your application. Unlike *Type*, *Kind* is mutable and application-defined. Changing the *Kind* has no effect on appearance or behavior at the engine level. You can query and search for *Blocks* by *Kind*. Common uses: - Categorizing template elements ("logo", "headline", "background") - Filtering blocks for custom UI - Automation workflows that process blocks by purpose ### Property A configurable attribute of a *Block*. *Properties* have types (`Bool`, `Int`, `Float`, `String`, `Color`, `Enum`) and paths like `text/fontSize` or `fill/image/imageFileURI`. Access *Properties* using type-specific getter and setter methods. Each *Block* type exposes different properties, which you can discover programmatically. See [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) for details. ## Assets and Resources ### Asset Think of *Assets* as media items that you can provide to your users: images, videos, audio files, fonts, stickers, or templates—anything that can be added to a design. *Assets* have metadata including: - **ID**: Unique identifier within an asset source - **Label**: Display name - **Meta**: Custom metadata (URI, dimensions, format) - **Thumbnail URI**: Preview image URL *Assets* are provided by *Asset Sources* and added through the UI or programmatically. ### Asset Source A provider of *Assets*. *Asset Sources* can be built-in (like the default sticker library) or custom. *Asset Sources* implement a query interface returning paginated results with search and filtering. - **Local Asset Source**: Assets defined in JSON, loaded at initialization - **Remote Asset Source**: Custom implementation fetching from external APIs Register *Asset Sources* with the *Engine* to make *Assets* available throughout your application. ### Resource Loaded data from an *Asset* URI. When you reference an image or video URL in a *Block*, the *Engine* fetches and caches the *Resource*. *Resources* include binary data and metadata for rendering. See [Resources](https://img.ly/docs/cesdk/mac-catalyst/concepts/resources-a58d71/) for details. ### Buffer A resizable container for arbitrary binary data. *Buffers* are useful for dynamically generated content that doesn't come from a URL, such as synthesized audio or programmatically created images. Create a *Buffer*, write data to it, and reference it by URI in *Block* properties. *Buffer* data is not serialized with scenes and changes cannot be undone. See [Buffers](https://img.ly/docs/cesdk/mac-catalyst/concepts/buffers-9c565b/) for details. ## Templating and Automation These terms describe dynamic content and reusable designs. ### Template A reusable design with predefined structure and styling. *Templates* typically contain *Placeholders* and *Variables* that users customize while maintaining overall layout and branding. *Templates* are scenes saved in a format that can be loaded and modified. ### Placeholder A *Block* marked for content replacement. When a *Block's* placeholder property is enabled, it signals that the *Block* expects user-provided content—an image drop zone or editable text field. *Placeholders* indicate which parts of a design should be customized versus fixed. See [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) for details. ### Variable A named value referenced in text blocks using `{{variableName}}` syntax. *Variables* enable data-driven design generation by populating templates with dynamic content. Define *Variables* at the scene level and reference them in text blocks. When a *Variable* value changes, all referencing text blocks update automatically. See [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) for details. ## Permissions and Scopes These terms relate to controlling what operations are allowed. ### Scope A permission setting controlling whether specific operations are allowed on a *Block*. *Scopes* enable fine-grained control over what users can modify—essential for template workflows where some elements should be editable and others locked. Common scopes: - `layer/move` — Allow or prevent moving - `layer/resize` — Allow or prevent resizing - `layer/rotate` — Allow or prevent rotation - `layer/visibility` — Allow or prevent hiding - `lifecycle/destroy` — Allow or prevent deletion - `editor/select` — Allow or prevent selection Enable or disable *Scopes* per *Block* to create controlled editing experiences. See [Lock Design Elements](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) for details. ### Role A preset collection of *Scope* settings. CE.SDK defines two built-in *Roles*: - **Creator**: Full access to all operations, for template authors - **Adopter**: Restricted access for end-users customizing templates *Roles* provide a convenient way to apply consistent permission sets. ## Layout and Units These terms relate to positioning and measurement. ### Design Unit The measurement unit for dimensions in a *Scene*. The choice affects how positions, sizes, and exports are interpreted. Options: - **Pixel**: Screen pixels, default for digital designs - **Millimeter**: Metric measurement for print - **Inch**: Imperial measurement for print Set the design unit at the scene level—all dimension values are interpreted in that unit. ### DPI (Dots Per Inch) Resolution setting affecting export quality and unit conversion. Higher DPI produces larger exports with more detail. The default is 300 DPI, suitable for print-quality output. DPI matters when working with physical units (millimeters, inches) as it determines how measurements translate to pixel dimensions during export. ## Operating Modes These terms describe how CE.SDK runs. ### Scene Mode The operational mode of a *Scene* determining available features: - **Design Mode**: Static designs. No timeline, no playback. Content arranged spatially on pages. - **Video Mode**: Time-based content. Includes timeline, playback controls, duration properties, and animations. Choose the mode when creating a scene—it affects which properties and operations are available. See [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) for details. ### Headless Mode Running CE.SDK without the built-in UI. Used for: - Server-side rendering and export - Automation pipelines - Custom UI implementations - Batch processing In *Headless Mode*, you work directly with *Engine* APIs without the visual editor. ## Events and State These terms relate to monitoring changes. ### Event / Subscription A callback mechanism for reacting to changes in the *Engine*. Subscribe to events and receive notifications when state changes. Common events: - Selection changes - Block state changes - History (undo/redo) changes Subscriptions return a cancellable token to stop listening when you no longer need notifications. See [Events](https://img.ly/docs/cesdk/mac-catalyst/concepts/events-353f97/) for details. ### Block State The current status of a *Block* indicating readiness or issues: - **Ready**: Normal state, no pending operations - **Pending**: Operation in progress, with optional progress value (0-1) - **Error**: Operation failed, with error type (`ImageDecoding`, `VideoDecoding`, `FileFetch`, etc.) *Block State* reflects the combined status of the *Block* and its attached *Fill*, *Shape*, and *Effects*. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Undo and History" description: "Manage undo and redo stacks in CE.SDK using multiple histories, callbacks, and API-based controls." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/concepts/undo-and-history-99479d/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Undo and History](https://img.ly/docs/cesdk/mac-catalyst/concepts/undo-and-history-99479d/) --- ```swift file=@cesdk_swift_examples/engine-guides-undo-and-history/UndoAndHistory.swift reference-only import IMGLYEngine @MainActor func undoAndHistory(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Subscribe to history updates. let historyTask = Task { for await kind in engine.editor.onHistoryUpdatedWithKind { switch kind { case .activated: print("Active history switched, scene unchanged.") case .updated: let canUndo = try engine.editor.canUndo() let canRedo = try engine.editor.canRedo() print("History updated — canUndo: \(canUndo), canRedo: \(canRedo)") @unknown default: break } } } let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 100) try engine.block.setHeight(block, value: 100) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.appendChild(to: page, child: block) if try engine.editor.canUndo() { try engine.editor.undo() } if try engine.editor.canRedo() { try engine.editor.redo() } try engine.block.setWidth(block, value: 200) try engine.editor.addUndoStep() if try engine.editor.canUndo() { try engine.editor.removeUndoStep() } let primaryHistory = engine.editor.getActiveHistory() let secondaryHistory = engine.editor.createHistory() engine.editor.setActiveHistory(secondaryHistory) // Operations here only affect secondaryHistory try engine.block.setWidth(block, value: 300) engine.editor.setActiveHistory(primaryHistory) engine.editor.destroyHistory(secondaryHistory) historyTask.cancel() } ``` Manage undo and redo operations in CE.SDK programmatically, subscribe to history changes, and use multiple independent history stacks for isolated editing contexts. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-undo-and-history) CE.SDK automatically tracks editing operations, enabling users to undo and redo changes. The engine creates undo steps for most operations automatically. You can also create multiple independent history stacks to isolate editing contexts — for example, separate histories for a main canvas and an overlay editor. ## Setup We start by creating a scene and page. The engine automatically creates a history stack when it initializes. ```swift highlight-undoAndHistory-setup let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) ``` ## Subscribing to History Changes Use `engine.editor.onHistoryUpdatedWithKind` to receive notifications when the history state changes. Each emission is a `HistoryUpdate` value that describes the kind of change: - `.updated` — the active history's snapshots changed because of an edit, an `addUndoStep()` call, or an `undo()`/`redo()`. The scene reflects the new state. - `.activated` — a different history buffer was made active via `setActiveHistory(_:)`. The undo/redo stack visible to the user changed, but no new snapshot was created and no undo or redo was applied. This separation matters for save-button or dirty-state logic: switching the active history (for example when toggling a preview mode) should not be treated as an unsaved change. ```swift highlight-undoAndHistory-subscribe // Subscribe to history updates. let historyTask = Task { for await kind in engine.editor.onHistoryUpdatedWithKind { switch kind { case .activated: print("Active history switched, scene unchanged.") case .updated: let canUndo = try engine.editor.canUndo() let canRedo = try engine.editor.canRedo() print("History updated — canUndo: \(canUndo), canRedo: \(canRedo)") @unknown default: break } } } ``` Cancel the `Task` when you no longer need notifications, such as when dismissing a view. ## Automatic Undo Step Creation Most editing operations automatically create undo steps. Adding a block to the scene records this operation in the history stack. ```swift highlight-undoAndHistory-createBlock let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 100) try engine.block.setHeight(block, value: 100) try engine.block.setFill(block, fill: engine.block.createFill(.color)) try engine.block.appendChild(to: page, child: block) ``` After creating the block, `canUndo()` returns `true`. ## Performing Undo and Redo Use `engine.editor.undo()` and `engine.editor.redo()` to revert or restore changes. Always check availability with `canUndo()` and `canRedo()` first. ```swift highlight-undoAndHistory-undo if try engine.editor.canUndo() { try engine.editor.undo() } ``` After undoing, `canRedo()` returns `true`. Call `redo()` to restore the change. ```swift highlight-undoAndHistory-redo if try engine.editor.canRedo() { try engine.editor.redo() } ``` ## Managing Undo Steps Manually Most operations are tracked automatically. For custom operations that the engine doesn't track, use `addUndoStep()` to create a checkpoint manually. ```swift highlight-undoAndHistory-manualStep try engine.block.setWidth(block, value: 200) try engine.editor.addUndoStep() ``` Use `removeUndoStep()` to discard the most recent undo step without affecting the redo stack. ```swift highlight-undoAndHistory-removeStep if try engine.editor.canUndo() { try engine.editor.removeUndoStep() } ``` ## Working with Multiple History Stacks CE.SDK supports multiple independent history stacks. This is useful when different parts of your app need separate undo/redo histories. Only the active history responds to undo/redo operations. ```swift highlight-undoAndHistory-multipleHistories let primaryHistory = engine.editor.getActiveHistory() let secondaryHistory = engine.editor.createHistory() engine.editor.setActiveHistory(secondaryHistory) // Operations here only affect secondaryHistory try engine.block.setWidth(block, value: 300) engine.editor.setActiveHistory(primaryHistory) engine.editor.destroyHistory(secondaryHistory) ``` - Create a stack with `createHistory()` and activate it with `setActiveHistory()` - Operations while a stack is active only affect that stack - A new stack starts from the current scene, including edits that have no undo step yet and the current selection - Switching back to a stack returns the scene to that stack's last undo step - Always call `destroyHistory()` when a stack is no longer needed to free resources ## API Reference | Method | Purpose | |--------|---------| | `engine.editor.createHistory()` | Create a new undo/redo history stack | | `engine.editor.destroyHistory(_:)` | Destroy a history stack and free resources | | `engine.editor.setActiveHistory(_:)` | Set a history stack as the active one | | `engine.editor.getActiveHistory()` | Get the currently active history stack | | `engine.editor.addUndoStep()` | Manually add a checkpoint to the undo stack | | `engine.editor.removeUndoStep()` | Remove the most recent undo step | | `engine.editor.undo()` | Revert to the previous history state | | `engine.editor.redo()` | Restore the next history state | | `engine.editor.canUndo()` | Check if an undo operation is available | | `engine.editor.canRedo()` | Check if a redo operation is available | | `engine.editor.onHistoryUpdatedWithKind` | Subscribe to history change notifications | ## Next Steps - [Events](https://img.ly/docs/cesdk/mac-catalyst/concepts/events-353f97/) — subscribe to block creation, update, and deletion events - [Editor State](https://img.ly/docs/cesdk/mac-catalyst/concepts/edit-modes-1f5b6c/) — track selection and edit mode changes - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/) — create and manage design scenes --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Conversion" description: "Convert designs into different formats such as PDF, PNG, MP4, and more using CE.SDK tools." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/conversion-c3fbb3/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/mac-catalyst/conversion-c3fbb3/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/conversion/overview-44dc58/) - Convert designs into different formats such as PDF, PNG, MP4, and more using CE.SDK tools. - [To Base64](https://img.ly/docs/cesdk/mac-catalyst/conversion/to-base64-39ff25/) - Convert CE.SDK exports to Base64-encoded strings for embedding in URLs, storing in databases, or transmitting via APIs. - [To Blob](https://img.ly/docs/cesdk/mac-catalyst/conversion/to-blob-4e6493/) - Export design blocks to binary data (Blob) in Swift for saving, uploading, or converting to images. - [To PNG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-png-f87eaf/) - Export CE.SDK designs to PNG format with lossless compression and full alpha support for graphics, UI elements, and content with transparency. - [To PDF](https://img.ly/docs/cesdk/mac-catalyst/conversion/to-pdf-eb937f/) - Convert your compositions to PDF for export and print. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Overview" description: "Convert designs into different formats such as PDF, PNG, MP4, and more using CE.SDK tools." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/conversion/overview-44dc58/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/mac-catalyst/conversion-c3fbb3/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/conversion/overview-44dc58/) --- CreativeEditor SDK (CE.SDK) allows you to export designs into a variety of formats, making it easy to prepare assets for web publishing, printing, storage, and other workflows. You can trigger conversions either programmatically through the SDK's API or manually using the built-in export options available in the UI. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## Supported Input and Output Formats CE.SDK accepts a range of input formats when working with designs, including: | Category | Supported Formats | | ------------- | ------------------------------------------------------------------ | | **Images** | `.png`, `.apng`, `.jpeg`, `.jpg`, `.gif`, `.webp`, `.svg`, `.bmp` | | **Video** | `.mp4` (H.264/AVC, H.265/HEVC), `.mov` (H.264/AVC, H.265/HEVC), `.webm` (VP8, VP9, AV1) | | **Audio** | `.wav`, `.mp3`, `.m4a`, `.mp4` (AAC or MP3), `.mov` (AAC or MP3), `.webm` (Opus or Vorbis) | | **Animation** | `.json` (Lottie), `.lottie` (dotLottie) | Animated images (`.gif` and `.apng`) import as a static first frame in design scenes and as a looping video fill in video scenes. A Lottie `.json` file or a dotLottie `.lottie` archive set as a video fill plays as one block. > **Note:** Need to import a format not listed here? CE.SDK allows you to create custom > importers for any file type by using our Scene and Block APIs > programmatically. When it comes to exporting or converting designs, the SDK supports the following output formats: | Category | Supported Formats | | ----------- | ------------------------------------------------------------------------------------ | | **Images** | `.png` (with transparency), `.jpeg`, `.webp`, `.tga` | | **Vector** | `.svg` (scalable vector graphics with text as paths) | | **Print** | `.pdf` (supports underlayer printing and spot colors) | | **Video** | `.mp4` (H.264 or H.265 on supported platforms with limited transparency support), `.webm` (VP8, VP9 or AV1 with Opus or Vorbis audio; VP8 and VP9 keep transparency on Android, iOS and macOS) | | **Scene** | `.imgly` or `.scene` (description of the scene without any assets) | | **Archive** | `.imgly` or `.zip` (fully self-contained archive that bundles the scene file with all assets) | > **Note:** Our custom cross-platform C++ based rendering and layout engine ensures > consistent output quality across devices. Each format serves different use cases, giving you the flexibility to adapt designs for your application's needs. ## Conversion Methods There are two main ways to trigger a conversion: - **Programmatically:**\ Use CE.SDK’s API methods to perform conversions directly from your code. This gives you full control over the export process, allowing you to customize settings, automate workflows, and integrate with other systems. - **Through the UI:**\ End users can trigger exports manually through CE.SDK’s built-in export options. The UI provides an intuitive way to export designs without writing code, ideal for non-technical users. Both methods provide access to core conversion features, ensuring you can choose the workflow that fits your project. ## Customization Options When exporting designs, CE.SDK offers several customization options to meet specific output requirements: - **Resolution and DPI Settings:**\ Adjust the resolution for raster exports like PNG to optimize for screen or print. - **Output Dimensions:**\ Define custom width and height settings for the exported file, independent of the original design size. - **File Quality:**\ For formats that support compression (such as PNG or PDF), you can control the quality level to balance file size and visual fidelity. - **Background Transparency:**\ Choose whether to preserve transparent backgrounds or export with a solid background color. - **Page Selection:**\ When exporting multi-page documents (e.g., PDFs), you can select specific pages or export all pages at once. - **Video Frame Selection:**\ When exporting from a video, you can select a specific frame to export as an image, allowing for thumbnail generation or frame captures. These options help ensure that your exported content is optimized for its intended platform, whether it's a website, a mobile app, or a print-ready document. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To Base64" description: "Convert CE.SDK exports to Base64-encoded strings for embedding in URLs, storing in databases, or transmitting via APIs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/conversion/to-base64-39ff25/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/mac-catalyst/conversion-c3fbb3/) > [To Base64](https://img.ly/docs/cesdk/mac-catalyst/conversion/to-base64-39ff25/) --- ```swift file=@cesdk_swift_examples/engine-guides-conversion-to-base64/ToBase64.swift reference-only import Foundation import IMGLYEngine @MainActor func toBase64(engine: Engine) async throws { let scene = try await engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) let graphic = try engine.block.create(.graphic) try engine.block.appendChild(to: page, child: graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(graphic, shape: rectShape) let colorFill = try engine.block.createFill(.color) try engine.block.setColor(colorFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) try engine.block.setFill(graphic, fill: colorFill) try engine.block.setWidth(graphic, value: 400) try engine.block.setHeight(graphic, value: 300) try engine.block.setPositionX(graphic, value: 200) try engine.block.setPositionY(graphic, value: 150) let blob = try await engine.block.export(page, mimeType: .png) let base64String = blob.base64EncodedString() let mimeType: MIMEType = .png let dataURI = "data:\(mimeType.rawValue);base64,\(blob.base64EncodedString())" let pngBlob = try await engine.block.export(page, mimeType: .png) let pngBase64 = pngBlob.base64EncodedString() let jpegBlob = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.8), ) let jpegBase64 = jpegBlob.base64EncodedString() let webpBlob = try await engine.block.export( page, mimeType: .webp, options: ExportOptions(webpQuality: 0.9), ) let webpBase64 = webpBlob.base64EncodedString() let pages = try engine.scene.getPages() var base64Results: [String] = [] for try await pageBlob in try await engine.block.export(pages, mimeType: .png) { base64Results.append(pageBlob.base64EncodedString()) } _ = base64String _ = dataURI _ = pngBase64 _ = jpegBase64 _ = webpBase64 _ = base64Results } ``` Convert CE.SDK exports to Base64-encoded strings for embedding in HTML, storing in databases, or transmitting via JSON APIs. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-conversion-to-base64) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Base64 encoding transforms binary image data into ASCII text. In Swift, CE.SDK's `engine.block.export()` returns a `Blob` (a typealias for `Data`), which you convert to Base64 using Foundation's built-in `base64EncodedString()` method. ## Export a Block to Base64 Export a design block as a PNG and convert the resulting `Data` to a Base64 string. ```swift highlight-toBase64-export let blob = try await engine.block.export(page, mimeType: .png) let base64String = blob.base64EncodedString() ``` The export returns a `Blob` (`Data`) containing the rendered image. Call `base64EncodedString()` to get the Base64 representation. This works with any `MIMEType` supported by the export method. ## Create a Data URI Construct a data URI by combining the MIME type with the Base64 string. Data URIs embed image data directly in HTML or CSS without separate file references. ```swift highlight-toBase64-dataURI let mimeType: MIMEType = .png let dataURI = "data:\(mimeType.rawValue);base64,\(blob.base64EncodedString())" ``` The resulting string follows the format `data:image/png;base64,...` and can be used anywhere a URL is expected, such as in web views or HTML templates. ## Work with Different MIME Types CE.SDK supports multiple image formats, each with format-specific quality options through `ExportOptions`. ```swift highlight-toBase64-mimeTypes let pngBlob = try await engine.block.export(page, mimeType: .png) let pngBase64 = pngBlob.base64EncodedString() let jpegBlob = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.8), ) let jpegBase64 = jpegBlob.base64EncodedString() let webpBlob = try await engine.block.export( page, mimeType: .webp, options: ExportOptions(webpQuality: 0.9), ) let webpBase64 = webpBlob.base64EncodedString() ``` | Format | Option | Default | Notes | |--------|--------|---------|-------| | PNG | `pngCompressionLevel` | `5` | Lossless, supports transparency | | JPEG | `jpegQuality` | `0.9` | Lossy, smaller file size, no transparency | | WebP | `webpQuality` | `1.0` | Modern format, good compression | > **Note:** Base64 increases data size by approximately 33%. For images larger than 100KB, consider storing the raw `Data` directly instead. ## Batch Process Multiple Pages Export all pages in a scene to Base64 strings using the batch export API. Pass the full array of page IDs to `engine.block.export(_:mimeType:)`, which returns an `AsyncThrowingStream` of blobs. ```swift highlight-toBase64-batch let pages = try engine.scene.getPages() var base64Results: [String] = [] for try await pageBlob in try await engine.block.export(pages, mimeType: .png) { base64Results.append(pageBlob.base64EncodedString()) } ``` The batch API reuses a single worker engine for all exports, making it more memory efficient than exporting pages individually. ## When to Use Base64 Base64 encoding is useful for: - Embedding images in HTML email templates or web views - Storing image data in text-only databases or `UserDefaults` - Transmitting images through JSON APIs that don't support binary data - Creating inline data URIs for CSS backgrounds For large images or file storage, write the `Data` directly to disk using `Data.write(to:)` instead. ## Next Steps - [Conversion Overview](https://img.ly/docs/cesdk/mac-catalyst/conversion/overview-44dc58/) — Overview of all conversion formats and options - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Explore all available export formats and configuration - [To PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) — Export designs to PDF format - [Compress Exports](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/compress-29105e/) — Optimize export file size and quality --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To Blob" description: "Export design blocks to binary data (Blob) in Swift for saving, uploading, or converting to images." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/conversion/to-blob-4e6493/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/mac-catalyst/conversion-c3fbb3/) > [To Binary Data](https://img.ly/docs/cesdk/mac-catalyst/conversion/to-blob-4e6493/) --- ```swift file=@cesdk_swift_examples/engine-guides-to-blob/ToBlob.swift reference-only import Foundation import IMGLYEngine @MainActor func toBlob(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL try engine.editor.setSettingString("basePath", value: baseURL.absoluteString) let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: sceneURL) let scene = try engine.scene.get()! let page = try engine.scene.getPages().first! let pngBlob: Blob = try await engine.block.export(page, mimeType: .png) let options = ExportOptions( jpegQuality: 0.8, targetWidth: 1920, targetHeight: 1080, ) let jpegBlob = try await engine.block.export(page, mimeType: .jpeg, options: options) let pages = try engine.scene.getPages() let stream = try await engine.block.export(pages, mimeType: .png) var blobs: [Blob] = [] for try await blob in stream { blobs.append(blob) } let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("export.png") try pngBlob.write(to: tempURL) } ``` Export design blocks to binary `Data` (aliased as `Blob`) for saving to disk, uploading to a server, or decoding for on-screen display. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-to-blob) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. CE.SDK's `engine.block.export()` method renders any design block — a page, scene, or individual graphic block — into a `Blob`. In Swift, `Blob` is a typealias for `Data`, so the result integrates directly with Foundation APIs like `FileManager` and `URLSession`, and can be decoded straight into an image for display. ## Export a Block to PNG Call `engine.block.export(_:mimeType:)` with a block ID and the desired format. The method returns a `Blob` containing the rendered output. ```swift highlight-toBlob-exportPng let pngBlob: Blob = try await engine.block.export(page, mimeType: .png) ``` Supported image MIME types include `.png`, `.jpeg`, `.webp`, `.tga`, and `.pdf`. ## Configure Export Options Pass an `ExportOptions` instance to control quality and dimensions. Options vary by format: | Option | Formats | Default | Description | | --- | --- | --- | --- | | `pngCompressionLevel` | PNG | `5` | 0-9, higher = smaller file, same quality | | `jpegQuality` | JPEG | `0.9` | 0-1, higher = better quality | | `webpQuality` | WebP | `1.0` | 0-1, higher = better quality | | `targetWidth` / `targetHeight` | All image | `0` | Scale to fill target size, keeping aspect ratio | ```swift highlight-toBlob-exportOptions let options = ExportOptions( jpegQuality: 0.8, targetWidth: 1920, targetHeight: 1080, ) let jpegBlob = try await engine.block.export(page, mimeType: .jpeg, options: options) ``` When both `targetWidth` and `targetHeight` are set, the block scales to fill the target rectangle while preserving its aspect ratio. ## Export Multiple Blocks To export several blocks efficiently, pass an array of IDs. The method returns an `AsyncThrowingStream` that yields one blob per block in order, reusing a single background engine for all exports. ```swift highlight-toBlob-exportStream let pages = try engine.scene.getPages() let stream = try await engine.block.export(pages, mimeType: .png) var blobs: [Blob] = [] for try await blob in stream { blobs.append(blob) } ``` This is more memory-efficient than calling `export` in a loop because instead of creating an internal worker engine instance for each export only a single instance is created once and reused across all blocks. ## Save to Disk Since `Blob` is `Data`, write it directly to a file URL with Foundation's `write(to:)`. ```swift highlight-toBlob-saveToFile let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("export.png") try pngBlob.write(to: tempURL) ``` You can also pass the blob to `URLSession` for uploading, or decode it as an image for display. ## Next Steps - [To PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) — Export scenes as single- or multi-page PDFs with print options. - [Conversion Overview](https://img.ly/docs/cesdk/mac-catalyst/conversion/overview-44dc58/) — See all supported export formats. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To PDF" description: "Convert your compositions to PDF for export and print." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/conversion/to-pdf-eb937f/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Conversion](https://img.ly/docs/cesdk/mac-catalyst/conversion-c3fbb3/) > [To PDF](https://img.ly/docs/cesdk/mac-catalyst/conversion/to-pdf-eb937f/) --- ```swift file=@cesdk_swift_examples/engine-guides-conversion-to-pdf/ConversionToPdf.swift reference-only import Foundation import IMGLYEngine @MainActor func conversionToPdf(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let exportsDirectory = FileManager.default.temporaryDirectory let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") try await engine.scene.create(fromImage: imageURL) guard let page = try engine.scene.getCurrentPage() else { return } let singleImagePdf = try await engine.block.export(page, mimeType: .pdf) try singleImagePdf.write(to: exportsDirectory.appendingPathComponent("single-image.pdf")) let imageURLs = [ baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_3.jpg"), ] let stackedScene = try engine.scene.create(sceneLayout: .verticalStack) let stack = try engine.block.find(byType: .stack)[0] for url in imageURLs { let stackedPage = try engine.block.create(.page) try engine.block.appendChild(to: stack, child: stackedPage) let imageFill = try engine.block.createFill(.image) try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: url) try engine.block.setFill(stackedPage, fill: imageFill) } let multiPagePdf = try await engine.block.export(stackedScene, mimeType: .pdf) try multiPagePdf.write(to: exportsDirectory.appendingPathComponent("multi-page.pdf")) try engine.block.setFloat(stackedScene, property: "scene/dpi", value: 150) let compatOptions = ExportOptions(exportPdfWithHighCompatibility: true) let compatPdf = try await engine.block.export(stackedScene, mimeType: .pdf, options: compatOptions) try compatPdf.write(to: exportsDirectory.appendingPathComponent("high-compatibility.pdf")) engine.editor.setSpotColor(name: "BrandUnderlay", r: 0.8, g: 0.8, b: 0.8) let underlayerOptions = ExportOptions( exportPdfWithHighCompatibility: true, exportPdfWithUnderlayer: true, underlayerSpotColorName: "BrandUnderlay", underlayerOffset: -2.0, ) let underlayerPdf = try await engine.block.export(stackedScene, mimeType: .pdf, options: underlayerOptions) try underlayerPdf.write(to: exportsDirectory.appendingPathComponent("with-underlayer.pdf")) try engine.block.setFloat(stackedScene, property: "scene/dpi", value: 300) let combinedOptions = ExportOptions( targetWidth: 2480, targetHeight: 3508, exportPdfWithHighCompatibility: true, exportPdfWithUnderlayer: true, underlayerSpotColorName: "BrandUnderlay", underlayerOffset: -2.0, ) let combinedPdf = try await engine.block.export(stackedScene, mimeType: .pdf, options: combinedOptions) try combinedPdf.write(to: exportsDirectory.appendingPathComponent("configured.pdf")) } ``` Convert images and multi-page designs to PDF programmatically. Load one or more image files, build a scene, and export the result as a print-ready PDF — all without presenting the editor UI. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-conversion-to-pdf) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. CE.SDK supports converting single or multiple images to PDF while allowing transformations such as cropping, rotating, and adding text before exporting. You can customize PDF output settings including resolution, compatibility, and underlayer for specialty printing. This guide covers converting a single image to PDF, combining multiple images into one multi-page PDF, and configuring output options such as DPI, high-compatibility mode, and underlayers. ## Convert to PDF Programmatically Use `engine.block.export(_:mimeType:options:)` with `MIMEType.pdf` to convert a scene or a single page to PDF. The method returns `Data` containing the PDF bytes, which you can write to disk with `Data.write(to:)`. ### Convert a Single Image to PDF Load an image into a fresh scene with `engine.scene.create(fromImage:)` and export the current page. ```swift highlight-conversionToPdf-singleImage let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") try await engine.scene.create(fromImage: imageURL) guard let page = try engine.scene.getCurrentPage() else { return } let singleImagePdf = try await engine.block.export(page, mimeType: .pdf) try singleImagePdf.write(to: exportsDirectory.appendingPathComponent("single-image.pdf")) ``` `engine.scene.create(fromImage:)` builds a scene with a single page whose fill is the loaded image. Passing that page ID to `engine.block.export(_:mimeType:)` produces a single-page PDF. To edit the image before exporting — cropping, rotating, adding text, and so on — you can query and mutate the page's fill and any additional blocks with the standard `engine.block` APIs. ### Combine Multiple Images into a Single PDF Create an empty scene with a vertical stack layout, add one page per image, and export the scene to produce a multi-page PDF. ```swift highlight-conversionToPdf-multiImage let imageURLs = [ baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_3.jpg"), ] let stackedScene = try engine.scene.create(sceneLayout: .verticalStack) let stack = try engine.block.find(byType: .stack)[0] for url in imageURLs { let stackedPage = try engine.block.create(.page) try engine.block.appendChild(to: stack, child: stackedPage) let imageFill = try engine.block.createFill(.image) try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: url) try engine.block.setFill(stackedPage, fill: imageFill) } let multiPagePdf = try await engine.block.export(stackedScene, mimeType: .pdf) try multiPagePdf.write(to: exportsDirectory.appendingPathComponent("multi-page.pdf")) ``` Exporting the scene (rather than an individual page) includes every page block that hangs off the stack. The pages share the same width; the stack manages their vertical arrangement. ## Configure PDF Output Settings Several `ExportOptions` fields shape the PDF output. Set them individually or combine them in one export call. ### Adjust DPI for Print Quality The scene's `scene/dpi` property controls the resolution at which bitmap images and rasterized effects are embedded in the PDF. It does not change the page size. ```swift highlight-conversionToPdf-dpi try engine.block.setFloat(stackedScene, property: "scene/dpi", value: 150) ``` Higher DPI values produce sharper output at the cost of larger files. The default is 300 DPI, which suits most print workflows. ### Enable High Compatibility Mode Set `exportPdfWithHighCompatibility` on `ExportOptions` to rasterize complex elements — bitmap images, gradients with transparency, and some effects — at the scene's DPI. This ensures consistent rendering across PDF viewers. ```swift highlight-conversionToPdf-highCompatibility let compatOptions = ExportOptions(exportPdfWithHighCompatibility: true) let compatPdf = try await engine.block.export(stackedScene, mimeType: .pdf, options: compatOptions) try compatPdf.write(to: exportsDirectory.appendingPathComponent("high-compatibility.pdf")) ``` The flag defaults to `true`. Disable it when you need vectors preserved and can verify that your target viewers render your gradients and effects correctly. Rasterizing at high DPI produces larger files but the tradeoff is more predictable output. ### Add an Underlayer for Specialty Printing Underlayers provide a base ink layer (typically white) for printing on transparent or non-white substrates like fabric, glass, or acrylic. The underlayer sits behind the design elements and is generated automatically from their contours. > **Caution:** Do not flatten the resulting PDF file or you will lose the underlayer shape, which sits behind your design. First define a spot color that represents the underlayer ink. The name must match what your print provider expects; the RGB values only provide a preview. ```swift highlight-conversionToPdf-spotColor engine.editor.setSpotColor(name: "BrandUnderlay", r: 0.8, g: 0.8, b: 0.8) ``` Then export with the underlayer options set. `underlayerOffset` adjusts the underlayer's size in design units — negative values shrink it inward to prevent visible edges from print misalignment. ```swift highlight-conversionToPdf-underlayer let underlayerOptions = ExportOptions( exportPdfWithHighCompatibility: true, exportPdfWithUnderlayer: true, underlayerSpotColorName: "BrandUnderlay", underlayerOffset: -2.0, ) let underlayerPdf = try await engine.block.export(stackedScene, mimeType: .pdf, options: underlayerOptions) try underlayerPdf.write(to: exportsDirectory.appendingPathComponent("with-underlayer.pdf")) ``` ### Combine All Options You can mix any of the PDF options in a single `ExportOptions` value. The example below resets `scene/dpi` to 300, sets target dimensions to A4 at 300 DPI (2480 × 3508 px), enables high compatibility, and generates an underlayer. ```swift highlight-conversionToPdf-combined try engine.block.setFloat(stackedScene, property: "scene/dpi", value: 300) let combinedOptions = ExportOptions( targetWidth: 2480, targetHeight: 3508, exportPdfWithHighCompatibility: true, exportPdfWithUnderlayer: true, underlayerSpotColorName: "BrandUnderlay", underlayerOffset: -2.0, ) let combinedPdf = try await engine.block.export(stackedScene, mimeType: .pdf, options: combinedOptions) try combinedPdf.write(to: exportsDirectory.appendingPathComponent("configured.pdf")) ``` `targetWidth` and `targetHeight` scale the block so it fills the requested size while preserving its aspect ratio, and both values are set together. Because the target-size scaling factors in the scene's DPI, resetting `scene/dpi` to 300 before this export keeps the output at the requested A4 dimensions. ## Troubleshooting | Symptom | Resolution | | --- | --- | | PDF file size is too large | Reduce the scene DPI or disable `exportPdfWithHighCompatibility` so bitmap images stay embedded at their original resolution. Set `pdfImageQuality` below `1.0` to encode the images that CE.SDK still has to rasterize as lossy JPEG. | | Gradients or effects render inconsistently across viewers | Enable `exportPdfWithHighCompatibility` so complex elements are rasterized at the scene DPI, producing consistent output across PDF viewers. | | Underlayer is missing from the printed result | Confirm the spot color name in `underlayerSpotColorName` matches the print provider's configuration exactly, and ensure the PDF was not flattened in post-processing (flattening removes the underlayer shape). | ## PDF Export Options The fields below are properties on `ExportOptions` you pass to `engine.block.export(_:mimeType:options:)`. | Option | Description | | --- | --- | | `exportPdfWithHighCompatibility` | Rasterize complex elements at scene DPI for consistent rendering across viewers. Defaults to `true`. | | `pdfImageQuality` | Encoding quality for images that CE.SDK has to rasterize. Values below the default of `1.0` encode them as lossy JPEG. | | `exportPdfWithUnderlayer` | Generate an underlayer from design contours. Defaults to `false`. | | `underlayerSpotColorName` | Spot color name for the underlayer ink. Required when `exportPdfWithUnderlayer` is `true`. | | `underlayerOffset` | Size adjustment in design units. Negative values shrink the underlayer inward. | | `targetWidth` | Target output width in pixels. Set together with `targetHeight`. | | `targetHeight` | Target output height in pixels. Set together with `targetWidth`. | ## API Reference | Method | Description | | --- | --- | | `engine.scene.create(fromImage:)` | Create a scene with a single page filled with the loaded image | | `engine.scene.create(sceneLayout:)` | Create an empty scene with the requested layout (for example `.verticalStack`) | | `engine.scene.getCurrentPage()` | Return the current page block ID | | `engine.block.find(byType:)` | Find all blocks of a given `DesignBlockType` (`.graphic`, `.stack`, `.page`, …) | | `engine.block.export(_:mimeType:options:)` | Export a block as PDF with the supplied options | | `engine.block.setFloat(_:property:value:)` | Set a `Float` property such as `scene/dpi` | | `engine.editor.setSpotColor(name:r:g:b:)` | Register an sRGB spot color used for the underlayer ink | | `ExportOptions(...)` | Configure export options including PDF-specific fields | ## Next Steps - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Compare all supported export formats - [Export for Printing](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-printing-bca896/) — Print workflows with DPI and color management - [Spot Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/spot-c3a150/) — Define and use spot colors in designs - [Export Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/) — Check device limits before exporting large designs - [Convert to PNG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-png-f87eaf/) — Convert designs to PNG for web and screen - [Conversion Overview](https://img.ly/docs/cesdk/mac-catalyst/conversion/overview-44dc58/) — Explore the full conversion workflow --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Audio" description: "Create audio blocks, extract tracks from video, control playback, generate waveforms, and manage audio timing in CE.SDK Engine for Swift." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio-2f700b/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio-2f700b/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-audio-audio/Audio.swift reference-only import Foundation import IMGLYEngine @MainActor func audio(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1080) try engine.block.setHeight(page, value: 1080) try engine.block.setDuration(page, duration: 30.0) let baseURL = try engine.guidesBaseURL let audioBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: audioBlock) try engine.block.setURL( audioBlock, property: "audio/fileURI", value: baseURL.appendingPathComponent("ly.img.audio/audios/far_from_home.m4a"), ) try await engine.block.forceLoadAVResource(audioBlock) let sourceDuration = try engine.block.getAVResourceTotalDuration(audioBlock) print("Audio source duration: \(sourceDuration) seconds") let videoBlock = try engine.block.create(.graphic) try engine.block.setShape(videoBlock, shape: try engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) try engine.block.setURL( videoFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent("ly.img.video/videos/pexels-kampus-production-8154913.mp4"), ) try engine.block.setFill(videoBlock, fill: videoFill) try engine.block.appendChild(to: page, child: videoBlock) try await engine.block.forceLoadAVResource(videoFill) let extractedAudio = try engine.block.createAudioFromVideo( videoFill, trackIndex: 0, options: AudioFromVideoOptions(keepTrimSettings: true, muteOriginalVideo: true), ) try engine.block.appendChild(to: page, child: extractedAudio) let allExtractedAudio = try engine.block.createAudiosFromVideo( videoFill, options: AudioFromVideoOptions(keepTrimSettings: true, muteOriginalVideo: false), ) print("Extracted \(allExtractedAudio.count) audio track(s) from video") // Append each extracted block to the scene hierarchy in your app where you want it to play. let trackCount = try engine.block.getAudioTrackCountFromVideo(videoFill) print("Video contains \(trackCount) audio track(s)") let tracks = try engine.block.getAudioInfoFromVideo(videoFill) for (listPosition, info) in tracks.enumerated() { print("Track at list position \(listPosition):") print(" codec: \(info.audioCodec)") print(" channels: \(info.channels)") print(" sample rate: \(info.sampleRate) Hz") print(" duration: \(info.audioDuration) s") print(" language: \(info.language)") print(" trackName: \(info.trackName)") print(" container trackIndex: \(info.trackIndex)") } try engine.block.setPlaying(page, enabled: true) let isScenePlaying = try engine.block.isPlaying(page) print("Scene playing: \(isScenePlaying)") try engine.block.setPlaybackTime(page, time: 3.0) try engine.block.setVolume(audioBlock, volume: 0.8) try engine.block.setMuted(audioBlock, muted: false) try engine.block.setPlaybackSpeed(audioBlock, speed: 1.0) try engine.block.setPlaying(page, enabled: false) try engine.block.setSoloPlaybackEnabled(audioBlock, enabled: true) try engine.block.setPlaying(page, enabled: true) // ... preview the audio block in isolation ... try engine.block.setPlaying(page, enabled: false) try engine.block.setSoloPlaybackEnabled(audioBlock, enabled: false) try engine.block.setTimeOffset(audioBlock, offset: 2.0) try engine.block.setDuration(audioBlock, duration: 10.0) let waveformStream = engine.block.generateAudioThumbnailSequence( audioBlock, samplesPerChunk: 3, timeRange: 0.0 ... 10.0, numberOfSamples: 9, numberOfChannels: 2, ) for try await thumbnail in waveformStream { print("Chunk \(thumbnail.chunkIndex) → \(thumbnail.samples.count) samples") } try engine.block.setTrimOffset(audioBlock, offset: 5.0) try engine.block.setTrimLength(audioBlock, length: 4.0) try engine.block.setLooping(audioBlock, looping: true) let exportBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: exportBlock) let audioBuffer = engine.editor.createBuffer() try engine.editor.setBufferLength(url: audioBuffer, length: 96000) try engine.block.setURL(exportBlock, property: "audio/fileURI", value: audioBuffer) let exportStream = try await engine.block.exportAudio( exportBlock, mimeType: .wav, options: AudioExportOptions(skipEncoding: true), ) for try await event in exportStream { switch event { case let .progress(rendered, encoded, total): print("Export progress: \(rendered)/\(total) rendered, \(encoded) encoded") case let .finished(audio): print("Exported \(audio.count) bytes of audio data") } } } ``` Add audio to your CE.SDK scenes with the Swift Engine API: create audio blocks from external files, extract tracks from video fills, control playback, manage timeline placement, generate waveform samples, and export audio data. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-audio-audio) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Audio blocks let you add background music, voice-overs, sound effects, and other standalone sound to scenes. CE.SDK also exposes video audio track counts, playback controls, trim controls, waveform generation, and audio-only export through the Engine block API. Playback examples require an `Engine` instance created with `audioContext: .auto`, which is the default. Engines created with `audioContext: .none` can still edit and export scenes, but they do not drive real-time audio playback. ## Use Cases Use the CE.SDK audio APIs when you need to add or manage: - Background music - Voice-overs - Sound effects - Podcast or narration tracks ## How Audio Works in CE.SDK CE.SDK represents standalone audio as `.audio` design blocks. Audio blocks attach to a page, reference their media through the `audio/fileURI` property, and use the same timeline properties as other time-based blocks. Each audio block can have: - A source URI for an external audio file. - Playback properties such as volume, mute state, playback time, and speed. - Timeline properties such as offset, duration, trim offset, and trim length. - Waveform sample data for a custom timeline UI. Extraction APIs create separate audio blocks from video fill tracks instead of modifying an existing audio block's source. ### Time-Based Properties Audio timing is expressed in seconds: - **Offset**: when the audio block starts relative to its parent. - **Duration**: how long the block stays active on the timeline. - **Trim offset**: where playback starts inside the source audio. - **Trim length**: how much of the source audio plays before it loops or stops. Use the looping APIs to choose that behavior. ### Waveforms Waveforms are sampled audio amplitudes you can render in your own UI. `generateAudioThumbnailSequence(_:samplesPerChunk:timeRange:numberOfSamples:numberOfChannels:)` returns an `AsyncThrowingStream` of chunks. Each chunk's `samples` array holds normalized amplitudes in the range `0` to `1`. Stereo requests interleave left and right samples. ### When to Create vs. Extract Audio Create an audio block when the source is an external file such as background music or a voice-over. Extract audio when the sound already lives inside a video fill and you need a separate audio block for trimming, muting the original video, or independent volume control. ## Examples The snippets below work against a scene with a page that has a timeline duration. Audio APIs run on the main thread through the same `Engine` instance as the rest of your scene edits. ### Create Audio Create a standalone audio block, attach it to the page, set its source URI, then load the resource before reading metadata such as the source duration. ```swift highlight-audio-create let audioBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: audioBlock) try engine.block.setURL( audioBlock, property: "audio/fileURI", value: baseURL.appendingPathComponent("ly.img.audio/audios/far_from_home.m4a"), ) try await engine.block.forceLoadAVResource(audioBlock) let sourceDuration = try engine.block.getAVResourceTotalDuration(audioBlock) print("Audio source duration: \(sourceDuration) seconds") ``` `forceLoadAVResource(_:)` is asynchronous; await it before calling `getAVResourceTotalDuration(_:)` so the engine has parsed the source. ### Extract or Count Video Audio Build a video fill block and wait for `forceLoadAVResource(_:)` to complete before extracting or counting video audio. The tabs below reuse the loaded `videoFill` created in this snippet. ```swift highlight-audio-videoFillSetup let videoBlock = try engine.block.create(.graphic) try engine.block.setShape(videoBlock, shape: try engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) try engine.block.setURL( videoFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent("ly.img.video/videos/pexels-kampus-production-8154913.mp4"), ) try engine.block.setFill(videoBlock, fill: videoFill) try engine.block.appendChild(to: page, child: videoBlock) try await engine.block.forceLoadAVResource(videoFill) ``` Extract one audio track from the video fill into a new audio block. With `AudioFromVideoOptions(keepTrimSettings: true, muteOriginalVideo: true)`, the extracted block plays like the video: the same part of the source at the same speed, for the clip's duration. It also copies the video's volume, mute state, looping, and audio fades. `muteOriginalVideo` silences the original fill so the audio plays from the extracted block alone. A video that plays faster than 3x is force muted, and so is its extracted audio. The audio plays again when its speed is 3x or less. ```swift highlight-audio-extract let extractedAudio = try engine.block.createAudioFromVideo( videoFill, trackIndex: 0, options: AudioFromVideoOptions(keepTrimSettings: true, muteOriginalVideo: true), ) try engine.block.appendChild(to: page, child: extractedAudio) ``` On iOS, the **Separate Audio** button, `InspectorBar.Buttons.separateAudio`, extracts every audio track of the selected video clip with the same options. See [Inspector Bar](#broken-link-8ca1cd). Use `createAudiosFromVideo(_:options:)` when the source may contain multiple audio tracks and you want each track to become its own audio block. ```swift highlight-audio-extractAll let allExtractedAudio = try engine.block.createAudiosFromVideo( videoFill, options: AudioFromVideoOptions(keepTrimSettings: true, muteOriginalVideo: false), ) print("Extracted \(allExtractedAudio.count) audio track(s) from video") // Append each extracted block to the scene hierarchy in your app where you want it to play. ``` Use `getAudioTrackCountFromVideo(_:)` before extraction when the source may be silent or carry multiple audio tracks. To choose a track by metadata, call `getAudioInfoFromVideo(_:)` after the same load step to read each track's `AudioTrackInfo`, which includes the codec string, channel count, sample rate, audio duration, packet and frame counts, track name, container track index, and ISO 639-2T language. Pass the returned list position to `createAudioFromVideo(_:trackIndex:options:)`; that parameter is the zero-based audio-track ordinal. Do not pass `AudioTrackInfo.trackIndex` directly, because that value is the original container track index and may not match the list position. ```swift highlight-audio-trackInfo let trackCount = try engine.block.getAudioTrackCountFromVideo(videoFill) print("Video contains \(trackCount) audio track(s)") let tracks = try engine.block.getAudioInfoFromVideo(videoFill) for (listPosition, info) in tracks.enumerated() { print("Track at list position \(listPosition):") print(" codec: \(info.audioCodec)") print(" channels: \(info.channels)") print(" sample rate: \(info.sampleRate) Hz") print(" duration: \(info.audioDuration) s") print(" language: \(info.language)") print(" trackName: \(info.trackName)") print(" container trackIndex: \(info.trackIndex)") } ``` ### Control Audio Playback Set play and pause state on the page so all time-based blocks stay synchronized. Set volume, mute state, playback speed, and per-block playback time on the audio block itself. ```swift highlight-audio-playback try engine.block.setPlaying(page, enabled: true) let isScenePlaying = try engine.block.isPlaying(page) print("Scene playing: \(isScenePlaying)") try engine.block.setPlaybackTime(page, time: 3.0) try engine.block.setVolume(audioBlock, volume: 0.8) try engine.block.setMuted(audioBlock, muted: false) try engine.block.setPlaybackSpeed(audioBlock, speed: 1.0) try engine.block.setPlaying(page, enabled: false) ``` Audio block speed accepts values from `0.25` to `3.0`. Changing the speed also changes how long a non-looping block takes on the timeline; looping blocks keep their authored duration regardless of speed. ### Solo Playback Preview a single audio block while the rest of the scene stays paused. Solo mode is useful for waveform pickers and trim editors that need to audition one source at a time. ```swift highlight-audio-soloPlayback try engine.block.setSoloPlaybackEnabled(audioBlock, enabled: true) try engine.block.setPlaying(page, enabled: true) // ... preview the audio block in isolation ... try engine.block.setPlaying(page, enabled: false) try engine.block.setSoloPlaybackEnabled(audioBlock, enabled: false) ``` Read the current state with `isSoloPlaybackEnabled(_:)`. ### Manage Audio Timing Use offset and duration to place the audio block on the scene timeline. Use trim offset and trim length to choose which part of the source file plays. ```swift highlight-audio-timing try engine.block.setTimeOffset(audioBlock, offset: 2.0) try engine.block.setDuration(audioBlock, duration: 10.0) ``` Trim the source range and enable looping when the trimmed source should repeat for as long as the block stays active. ```swift highlight-audio-trim try engine.block.setTrimOffset(audioBlock, offset: 5.0) try engine.block.setTrimLength(audioBlock, length: 4.0) try engine.block.setLooping(audioBlock, looping: true) ``` Load the audio resource with `forceLoadAVResource(_:)` before trimming so CE.SDK can read the source duration. Trim values are silently clamped to the available range, so loading first lets you compute and pass values the user expects to see. ### Generate Audio Thumbnails Waveform generation returns an `AsyncThrowingStream`. Choose `samplesPerChunk`, a `timeRange`, the total number of samples, and the number of channels. Iterate the stream with `for try await` and render each chunk's `samples` in your own timeline component. ```swift highlight-audio-waveform let waveformStream = engine.block.generateAudioThumbnailSequence( audioBlock, samplesPerChunk: 3, timeRange: 0.0 ... 10.0, numberOfSamples: 9, numberOfChannels: 2, ) for try await thumbnail in waveformStream { print("Chunk \(thumbnail.chunkIndex) → \(thumbnail.samples.count) samples") } ``` Cancel an in-flight stream by terminating the consuming `Task`; the binding cancels the underlying thumbnail generation automatically. ### Export Audio Export an audio block to bytes with `exportAudio(_:mimeType:options:)`. The call returns an `AsyncThrowingStream` that yields `.progress(renderedFrames:encodedFrames:totalFrames:)` events during render and a final `.finished(audio:)` event carrying the exported `Blob`. The example below backs the export block with an in-memory buffer so the snippet runs without a network request: `engine.editor.createBuffer()` returns a `buffer://` URL the audio block reads from, and `setBufferLength(url:length:)` reserves capacity in bytes. `skipEncoding: true` copies the raw samples of the buffer into a WAV file without running the mixer; with `false` (the default) the engine mixes the block into the WAV file. Pass `MIMEType.mp4` instead of `.wav` for an MP4 container. `skipEncoding` has no effect on an MP4 export. ```swift highlight-audio-export let exportBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: exportBlock) let audioBuffer = engine.editor.createBuffer() try engine.editor.setBufferLength(url: audioBuffer, length: 96000) try engine.block.setURL(exportBlock, property: "audio/fileURI", value: audioBuffer) let exportStream = try await engine.block.exportAudio( exportBlock, mimeType: .wav, options: AudioExportOptions(skipEncoding: true), ) for try await event in exportStream { switch event { case let .progress(rendered, encoded, total): print("Export progress: \(rendered)/\(total) rendered, \(encoded) encoded") case let .finished(audio): print("Exported \(audio.count) bytes of audio data") } } ``` To persist the full scene including audio sources, see the scene persistence APIs covered in the export guides. ## API Reference | Category | API | Purpose | | --- | --- | --- | | Engine audio context | `Engine(context:_, audioContext: .auto, license: _)` | Create an `Engine` that can drive a hardware audio device | | Create blocks | `engine.block.create(.audio)` | Create a standalone audio block | | Create blocks | `engine.block.create(.graphic)` | Create a block that can host the video fill | | Create blocks | `engine.block.createShape(.rect)` | Create the shape for the video block | | Create blocks | `engine.block.createFill(.video)` | Create a video fill the audio extraction APIs accept | | Scene hierarchy | `engine.block.appendChild(to:child:)` | Attach audio, video, or extracted blocks to the scene hierarchy | | Assign sources | `engine.block.setString(_:property: "audio/fileURI", value: _)` | Attach an audio file URI to an audio block | | Assign sources | `engine.block.setString(_:property: "fill/video/fileURI", value: _)` | Attach a video file URI to a video fill | | Video fill setup | `engine.block.setShape(_:shape:)` | Assign a shape to the video block | | Video fill setup | `engine.block.setFill(_:fill:)` | Assign the loaded video fill to the video block | | Extract video audio | `engine.block.createAudioFromVideo(_:trackIndex:options:)` | Extract one audio track by zero-based audio-track ordinal from a video fill | | Extract video audio | `engine.block.createAudiosFromVideo(_:options:)` | Extract every audio track from a video fill | | Extract video audio | `AudioFromVideoOptions(keepTrimSettings:muteOriginalVideo:)` | Make the audio play like the video, and mute the source | | Count video audio | `engine.block.getAudioTrackCountFromVideo(_:)` | Count the audio tracks in a video fill | | Inspect video audio | `engine.block.getAudioInfoFromVideo(_:)` | Read `AudioTrackInfo` metadata; use the returned list position for extraction because `AudioTrackInfo.trackIndex` is the container track index | | Playback | `engine.block.setPlaying(_:enabled:)` | Start or stop playback for a page or playable block | | Playback | `engine.block.isPlaying(_:)` | Read the current play or pause state | | Playback | `engine.block.supportsPlaybackControl(_:)` | Check whether playback control APIs apply to a block | | Playback | `engine.block.setPlaybackTime(_:time:)` | Move playback to a timeline position | | Playback | `engine.block.getPlaybackTime(_:)` | Read the current playback time | | Playback | `engine.block.supportsPlaybackTime(_:)` | Check whether a block exposes a playback time cursor | | Playback | `engine.block.setVolume(_:volume:)` | Set volume from `0.0` to `1.0` | | Playback | `engine.block.getVolume(_:)` | Read the current volume | | Playback | `engine.block.setMuted(_:muted:)` | Mute or unmute audio | | Playback | `engine.block.isMuted(_:)` | Read whether audio is muted | | Playback | `engine.block.isForceMuted(_:)` | Read whether the engine is muting the block (for example a video fill played above 3.0x) | | Playback | `engine.block.setPlaybackSpeed(_:speed:)` | Set audio speed from `0.25` to `3.0` | | Playback | `engine.block.getPlaybackSpeed(_:)` | Read the current playback speed | | Playback | `engine.block.setSoloPlaybackEnabled(_:enabled:)` | Preview one block while the rest of the scene stays paused | | Playback | `engine.block.isSoloPlaybackEnabled(_:)` | Read whether solo playback is enabled | | Timing | `engine.block.supportsTimeOffset(_:)` | Check whether a block can be positioned on its parent's timeline | | Timing | `engine.block.setTimeOffset(_:offset:)` | Move the audio block on the timeline | | Timing | `engine.block.getTimeOffset(_:)` | Read where the audio block starts on the timeline | | Timing | `engine.block.supportsDuration(_:)` | Check whether a block exposes an active timeline duration | | Timing | `engine.block.setDuration(_:duration:)` | Set the active block duration | | Timing | `engine.block.getDuration(_:)` | Read the active block duration | | Timing | `engine.block.supportsTrim(_:)` | Check whether a block or fill exposes trim controls | | Timing | `engine.block.setTrimOffset(_:offset:)` | Start playback inside the source audio | | Timing | `engine.block.getTrimOffset(_:)` | Read the source trim start | | Timing | `engine.block.setTrimLength(_:length:)` | Limit the source range used for playback | | Timing | `engine.block.getTrimLength(_:)` | Read the source trim length | | Timing | `engine.block.setLooping(_:looping:)` | Loop the trimmed source while the block stays active | | Timing | `engine.block.isLooping(_:)` | Read whether the source loops or stops | | Resources | `engine.block.forceLoadAVResource(_:)` | Load audio or video metadata before querying it | | Resources | `engine.block.getAVResourceTotalDuration(_:)` | Read the loaded audio or video source duration | | Waveforms | `engine.block.generateAudioThumbnailSequence(_:samplesPerChunk:timeRange:numberOfSamples:numberOfChannels:)` | Generate waveform sample chunks as an `AsyncThrowingStream` | | Export | `engine.block.exportAudio(_:mimeType:options:)` | Export an audio block to bytes; returns an `AsyncThrowingStream` | | Export | `AudioExportOptions(sampleRate:numberOfChannels:timeOffset:duration:skipEncoding:)` | Configure the audio export sample rate, channels, time range, and encoding | | Export | `engine.editor.createBuffer()` | Create an in-memory `buffer://` URL that an audio block can read from | | Export | `engine.editor.setBufferLength(url:length:)` | Reserve buffer capacity for the audio block to read | | Export | `engine.block.setURL(_:property:value:)` | Bind a URL (including a `buffer://` URL) to a block property | ## Next Steps - [CE.SDK API Reference](https://img.ly/docs/cesdk/mac-catalyst/api-reference/overview-8f24e1/) — Browse the complete IMGLYEngine API surface. - [Add Music](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-music-5b182c/) — Add background music and audio tracks to video projects. - [Add Sound Effects](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-sound-effects-9e984e/) — Generate sound effects programmatically from raw PCM data using audio buffers. - [Adjust Audio Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/) — Control playback levels, mute audio, and balance multiple audio sources. - [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-speed-908d57/) — Create slow-motion, time-stretched, and fast-forward audio effects. - [Loop Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/loop-937be7/) — Create seamless repeating audio for background music and sound effects. - [Record Voiceover](#broken-link-07e8e1) — On iOS, let users capture voiceover clips directly in the editor UI. --- ## Related Pages - [Add Sound Effects](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-sound-effects-9e984e/) - Generate sound effects programmatically with CE.SDK audio buffers — create chimes, melodies, and alert tones from raw PCM data and place them on the timeline. - [Add Music](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-music-5b182c/) - Add background music and audio tracks to video projects programmatically using CE.SDK's Engine API for Swift. - [Adjust Audio Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/) - Learn how to adjust audio volume in CE.SDK Engine for Swift to control playback levels, mute audio, and balance multiple audio sources in video projects. - [Fade Audio In and Out](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/fade-a4d7e2/) - Learn how to fade audio in and out in CE.SDK Engine for Swift, choose an easing curve, read the fade configuration back, and fade the audio of video fills. - [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-speed-908d57/) - Control audio playback speed from quarter-speed (0.25x) to triple-speed (3.0x) using the CE.SDK engine API. - [Loop Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/loop-937be7/) - Control audio looping behavior programmatically using CE.SDK's headless engine for Swift-based audio processing and automated content workflows. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Add Music" description: "Add background music and audio tracks to video projects programmatically using CE.SDK's Engine API for Swift." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-music-5b182c/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio-2f700b/) > [Add Music](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-music-5b182c/) --- Add background music and audio tracks to video projects programmatically using CE.SDK's Engine API for Swift. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-audio-add-music) Audio blocks are standalone time-based blocks that play alongside video content, independent of video fills. Create audio blocks for background music, voiceovers, and sound effects with separate control over each track. Audio blocks support M4A, MP3, and WAV formats. ```swift file=@cesdk_swift_examples/engine-guides-create-audio-add-music/AddMusic.swift reference-only import Foundation import IMGLYEngine @MainActor func addMusic(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 30) let baseURL = try engine.guidesBaseURL // Create an audio block and point it at an audio file. let audioBlock = try engine.block.create(.audio) try engine.block.setURL( audioBlock, property: "audio/fileURI", value: baseURL.appendingPathComponent("ly.img.audio/audios/far_from_home.m4a"), ) try engine.block.appendChild(to: page, child: audioBlock) // Wait for the audio resource to load before reading metadata such as duration. try await engine.block.forceLoadAVResource(audioBlock) // Read the total audio file length and offset playback to start three seconds in. let totalDuration = try engine.block.getAVResourceTotalDuration(audioBlock) try engine.block.setTimeOffset(audioBlock, offset: 3) try engine.block.setDuration(audioBlock, duration: min(totalDuration, 15)) // Set the block to 50% volume. Values range from 0.0 (silent) to 1.0 (full volume). try engine.block.setVolume(audioBlock, volume: 0.5) let currentVolume = try engine.block.getVolume(audioBlock) print(String(format: "Background music volume: %.0f%%", currentVolume * 100)) // Register the audio asset source by loading its content.json. The returned ID // matches the `id` field in the JSON (here, `ly.img.audio`). let audioSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.audio/content.json"), ) // Query the first page of audio assets from the source. let results = try await engine.asset.findAssets( sourceID: audioSourceID, query: .init(query: nil, page: 0, perPage: 10), ) print("Available audio assets: \(results.total)") // Apply an asset result to add a new audio block configured from the asset's metadata. if let firstAsset = results.assets.first { let appliedBlock = try await engine.asset.apply( sourceID: audioSourceID, assetResult: firstAsset, ) print("Created audio block from asset: \(appliedBlock.map(String.init) ?? "nil")") } // Layer a second track from a different source on top of the first audio block. // The two blocks play simultaneously while their time ranges overlap. let backgroundAudio = try engine.block.create(.audio) try engine.block.setURL( backgroundAudio, property: "audio/fileURI", value: baseURL.appendingPathComponent("ly.img.audio/audios/dance_harder.m4a"), ) try engine.block.appendChild(to: page, child: backgroundAudio) try engine.block.setTimeOffset(backgroundAudio, offset: 10) try engine.block.setDuration(backgroundAudio, duration: 8) try engine.block.setVolume(backgroundAudio, volume: 0.2) // Iterate every audio block in the scene and read its current configuration. let audioBlocks = try engine.block.find(byType: .audio) for block in audioBlocks { let uri = try engine.block.getString(block, property: "audio/fileURI") let offset = try engine.block.getTimeOffset(block) let duration = try engine.block.getDuration(block) let volume = try engine.block.getVolume(block) print(String(format: "Audio %u — offset %.1fs, duration %.1fs, volume %.0f%%, uri %@", block, offset, duration, volume * 100, uri)) } // Destroy an audio block to remove it from the scene and free its resources. try engine.block.destroy(backgroundAudio) } ``` This guide covers how to create and configure audio blocks using the Block API, query audio from the built-in asset library, layer multiple tracks, and manage audio blocks in a scene. ## Programmatic Audio Creation ### Create Audio Block Create an audio block with `create(_:)`, assign the source file with `setString(_:property:value:)` using the `audio/fileURI` property, and append it to a page with `appendChild(to:child:)`. Audio blocks must be children of a page to participate in the timeline. ```swift highlight-addMusic-createAudioBlock // Create an audio block and point it at an audio file. let audioBlock = try engine.block.create(.audio) try engine.block.setURL( audioBlock, property: "audio/fileURI", value: baseURL.appendingPathComponent("ly.img.audio/audios/far_from_home.m4a"), ) try engine.block.appendChild(to: page, child: audioBlock) ``` The source URI can point to any accessible URL or local file. CE.SDK supports M4A, MP3, and WAV formats. ### Configure Time Position Use `setTimeOffset(_:offset:)` to control when audio starts and `setDuration(_:duration:)` to control how long it plays. Call `forceLoadAVResource(_:)` first to ensure the audio file is loaded before reading metadata such as total duration. ```swift highlight-addMusic-configureTimeline // Wait for the audio resource to load before reading metadata such as duration. try await engine.block.forceLoadAVResource(audioBlock) // Read the total audio file length and offset playback to start three seconds in. let totalDuration = try engine.block.getAVResourceTotalDuration(audioBlock) try engine.block.setTimeOffset(audioBlock, offset: 3) try engine.block.setDuration(audioBlock, duration: min(totalDuration, 15)) ``` `getAVResourceTotalDuration(_:)` returns the length of the source audio file in seconds. Use it to clamp the playback duration to the available content or compute timing relative to the file length. ### Configure Volume Set volume using `setVolume(_:volume:)` with a `Float` value between `0.0` (silent) and `1.0` (full volume). Volume applies during playback and export. ```swift highlight-addMusic-configureVolume // Set the block to 50% volume. Values range from 0.0 (silent) to 1.0 (full volume). try engine.block.setVolume(audioBlock, volume: 0.5) let currentVolume = try engine.block.getVolume(audioBlock) print(String(format: "Background music volume: %.0f%%", currentVolume * 100)) ``` Read the current level back with `getVolume(_:)`. For a deeper dive into mixing, muting, and force-mute states, see [Adjust Audio Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/). ## Working with Audio Assets ### Query Audio Library Register the audio asset source from its `content.json` manifest using `addLocalAssetSourceFromJSON(_:matcher:)`. The call returns the source ID declared inside the JSON, which you then pass to `findAssets(sourceID:query:)` to list tracks. ```swift highlight-addMusic-queryAudioAssets // Register the audio asset source by loading its content.json. The returned ID // matches the `id` field in the JSON (here, `ly.img.audio`). let audioSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.audio/content.json"), ) // Query the first page of audio assets from the source. let results = try await engine.asset.findAssets( sourceID: audioSourceID, query: .init(query: nil, page: 0, perPage: 10), ) print("Available audio assets: \(results.total)") ``` Each `AssetResult` includes metadata such as duration, file URI, and a thumbnail URL, which you can use to build selection interfaces or filter tracks programmatically. For production use, self-host the JSON and assets and load them from your own URL rather than from the IMG.LY CDN. Apply an asset result with `apply(sourceID:assetResult:)` to add a new audio block configured from the asset's metadata in a single call. ```swift highlight-addMusic-applyAsset // Apply an asset result to add a new audio block configured from the asset's metadata. if let firstAsset = results.assets.first { let appliedBlock = try await engine.asset.apply( sourceID: audioSourceID, assetResult: firstAsset, ) print("Created audio block from asset: \(appliedBlock.map(String.init) ?? "nil")") } ``` `apply(sourceID:assetResult:)` returns the new block's `DesignBlockID` (or `nil` if the asset source did not produce a block). The block is automatically appended to the scene with the source URI and duration prepared from the asset. ## Adding Multiple Audio Tracks Add multiple audio blocks to a page to layer tracks. Each block carries its own offset, duration, and volume — a common pattern is to keep voiceover or dialogue at higher levels (0.8–1.0) and background music at lower levels (0.2–0.5) for a balanced mix. ```swift highlight-addMusic-multipleAudio // Layer a second track from a different source on top of the first audio block. // The two blocks play simultaneously while their time ranges overlap. let backgroundAudio = try engine.block.create(.audio) try engine.block.setURL( backgroundAudio, property: "audio/fileURI", value: baseURL.appendingPathComponent("ly.img.audio/audios/dance_harder.m4a"), ) try engine.block.appendChild(to: page, child: backgroundAudio) try engine.block.setTimeOffset(backgroundAudio, offset: 10) try engine.block.setDuration(backgroundAudio, duration: 8) try engine.block.setVolume(backgroundAudio, volume: 0.2) ``` Audio blocks play simultaneously when their time ranges overlap, so you can mix several tracks at the same point on the timeline. ## Managing Audio Blocks ### List Audio Blocks Use `find(byType:)` with `DesignBlockType.audio` to retrieve every audio block in the scene. This is useful for building audio management interfaces or for batch operations. ```swift highlight-addMusic-listAudioBlocks // Iterate every audio block in the scene and read its current configuration. let audioBlocks = try engine.block.find(byType: .audio) for block in audioBlocks { let uri = try engine.block.getString(block, property: "audio/fileURI") let offset = try engine.block.getTimeOffset(block) let duration = try engine.block.getDuration(block) let volume = try engine.block.getVolume(block) print(String(format: "Audio %u — offset %.1fs, duration %.1fs, volume %.0f%%, uri %@", block, offset, duration, volume * 100, uri)) } ``` ### Remove Audio Call `destroy(_:)` to remove a block from the scene and free its resources. Destroying a block automatically detaches it from its parent. ```swift highlight-addMusic-removeAudio // Destroy an audio block to remove it from the scene and free its resources. try engine.block.destroy(backgroundAudio) ``` Destroy blocks that are no longer needed to keep the scene clean and prevent unused resources from accumulating, especially when working with many audio files. ## Troubleshooting ### Duration Returns Zero Call `forceLoadAVResource(_:)` before reading `getAVResourceTotalDuration(_:)`. The audio metadata must be loaded before the resource size and duration are available. ### Audio Not Playing Verify the audio block is appended to a page and the page has sufficient duration. Confirm the audio URI is reachable and uses a supported format (M4A, MP3, or WAV). ### Volume Changes Not Applied Set volume before export — the value is captured into the rendered output. If a block sounds silent at a non-zero volume, check `isMuted(_:)` and `isForceMuted(_:)`. See [Adjust Audio Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/) for a full walkthrough of mute states. ## API Reference | Method | Category | Purpose | | ----------------------------------------------------- | -------- | ------------------------------------ | | `block.create(.audio)` | Block | Create a new audio block | | `block.setString(_:property:value:)` (`audio/fileURI`)| Block | Set the audio source file | | `block.appendChild(to:child:)` | Block | Append audio to a page | | `block.forceLoadAVResource(_:)` | Block | Load audio metadata | | `block.getAVResourceTotalDuration(_:)` | Block | Get total audio file duration | | `block.setTimeOffset(_:offset:)` | Block | Set when audio starts on the timeline| | `block.setDuration(_:duration:)` | Block | Set audio playback duration | | `block.setVolume(_:volume:)` | Block | Set volume (0.0 to 1.0) | | `block.getVolume(_:)` | Block | Get current volume level | | `block.find(byType:)` | Block | Find blocks by type | | `block.destroy(_:)` | Block | Destroy an audio block | | `asset.addLocalAssetSourceFromJSON(_:matcher:)` | Asset | Register an asset source from JSON | | `asset.findAssets(sourceID:query:)` | Asset | Query assets from a source | | `asset.apply(sourceID:assetResult:)` | Asset | Apply an asset to create a block | ## Next Steps - [Adjust Audio Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/) — Control audio playback levels and balance multiple sources - [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-speed-908d57/) — Create slow-motion, time-stretched, and fast-forward audio effects - [Add Sound Effects](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-sound-effects-9e984e/) — Add sound effects at specific moments - [Loop Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/loop-937be7/) — Loop audio tracks for continuous playback - [Record Voiceover](#broken-link-07e8e1) — On iOS, let users capture voiceover clips directly in the editor UI --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Add Sound Effects" description: "Generate sound effects programmatically with CE.SDK audio buffers — create chimes, melodies, and alert tones from raw PCM data and place them on the timeline." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-sound-effects-9e984e/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio-2f700b/) > [Add Sound Effects](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-sound-effects-9e984e/) --- Generate sound effects programmatically using buffers with arbitrary audio data. Create notification chimes, alert tones, and melodies without external files. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-audio-add-sound-effects) CE.SDK lets you create audio from code using buffers. This approach generates sound effects dynamically without external files — useful for notification tones, procedural audio, or any scenario where you need to synthesize audio at runtime. ```swift file=@cesdk_swift_examples/engine-guides-create-audio-add-sound-effects/AddSoundEffects.swift reference-only import Foundation import IMGLYEngine @MainActor func addSoundEffects(engine: Engine) async throws { func createWavData( sampleRate: Int, durationSeconds: Double, generator: (Double) -> Double, ) -> Data { let bitsPerSample: UInt16 = 16 let channels: UInt16 = 2 // Stereo output let numSamples = Int(durationSeconds * Double(sampleRate)) let dataSize = UInt32(numSamples * Int(channels) * Int(bitsPerSample / 8)) var data = Data(capacity: 44 + Int(dataSize)) func writeLE16(_ value: UInt16) { var le = value.littleEndian withUnsafeBytes(of: &le) { data.append(contentsOf: $0) } } func writeLE32(_ value: UInt32) { var le = value.littleEndian withUnsafeBytes(of: &le) { data.append(contentsOf: $0) } } func writeSample(_ value: Int16) { var le = value.littleEndian withUnsafeBytes(of: &le) { data.append(contentsOf: $0) } } // RIFF chunk descriptor data.append(contentsOf: [0x52, 0x49, 0x46, 0x46]) // "RIFF" writeLE32(36 + dataSize) // File size - 8 data.append(contentsOf: [0x57, 0x41, 0x56, 0x45]) // "WAVE" // fmt sub-chunk data.append(contentsOf: [0x66, 0x6D, 0x74, 0x20]) // "fmt " writeLE32(16) // Sub-chunk size (16 for PCM) writeLE16(1) // Audio format (1 = PCM) writeLE16(channels) writeLE32(UInt32(sampleRate)) writeLE32(UInt32(sampleRate) * UInt32(channels) * UInt32(bitsPerSample / 8)) writeLE16(channels * (bitsPerSample / 8)) // Block align writeLE16(bitsPerSample) // data sub-chunk data.append(contentsOf: [0x64, 0x61, 0x74, 0x61]) // "data" writeLE32(dataSize) // Generate audio samples — duplicate mono value to both stereo channels. for i in 0 ..< numSamples { let time = Double(i) / Double(sampleRate) let value = generator(time) let clamped = max(-1.0, min(1.0, value)) let sample = Int16((clamped * 32767.0).rounded()) writeSample(sample) // Left channel writeSample(sample) // Right channel } return data } func adsr( time: Double, noteStart: Double, noteDuration: Double, attack: Double, decay: Double, sustain: Double, release: Double, ) -> Double { let t = time - noteStart guard t >= 0 else { return 0 } let noteEnd = noteDuration - release if t < attack { // Attack phase: ramp up from 0 to 1 return t / attack } else if t < attack + decay { // Decay phase: ramp down from 1 to sustain level return 1 - ((t - attack) / decay) * (1 - sustain) } else if t < noteEnd { // Sustain phase: hold at sustain level return sustain } else if t < noteDuration { // Release phase: ramp down from sustain to 0 return sustain * (1 - (t - noteEnd) / release) } return 0 } struct Note { let freq: Double let start: Double let duration: Double } struct SoundEffect { let notes: [Note] let totalDuration: Double } // Musical note frequencies (Hz) for the 4th and 5th octaves. enum Notes { static let c4 = 261.63 static let e4 = 329.63 static let g4 = 392.0 static let a4 = 440.0 static let c5 = 523.25 static let d5 = 587.33 static let e5 = 659.25 static let f5 = 698.46 static let g5 = 783.99 static let a5 = 880.0 } // Sound effect 1: Ascending "success" fanfare with overlapping arpeggio and chord. let successChime = SoundEffect( notes: [ Note(freq: Notes.c4, start: 0.0, duration: 0.4), Note(freq: Notes.e4, start: 0.1, duration: 0.4), Note(freq: Notes.g4, start: 0.2, duration: 0.5), Note(freq: Notes.c5, start: 0.35, duration: 1.65), Note(freq: Notes.e5, start: 0.4, duration: 1.6), Note(freq: Notes.g5, start: 0.45, duration: 1.55), ], totalDuration: 2.0, ) // Sound effect 2: Gentle notification melody that resolves pleasantly. let notificationMelody = SoundEffect( notes: [ Note(freq: Notes.e5, start: 0.0, duration: 0.4), Note(freq: Notes.g5, start: 0.25, duration: 0.5), Note(freq: Notes.a5, start: 0.6, duration: 0.3), Note(freq: Notes.g5, start: 0.85, duration: 0.4), Note(freq: Notes.e5, start: 1.15, duration: 0.85), ], totalDuration: 2.0, ) // Sound effect 3: Descending alert tone that grabs attention. let alertTone = SoundEffect( notes: [ Note(freq: Notes.a5, start: 0.0, duration: 0.25), Note(freq: Notes.a5, start: 0.3, duration: 0.25), Note(freq: Notes.f5, start: 0.6, duration: 0.4), Note(freq: Notes.d5, start: 0.9, duration: 0.5), Note(freq: Notes.a4, start: 1.3, duration: 0.7), ], totalDuration: 2.0, ) let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) // Total duration: 3 effects × 2s + 2 gaps × 0.5s = 7s let effectDuration = 2.0 let gapDuration = 0.5 let totalDuration = 3 * effectDuration + 2 * gapDuration try engine.block.setDuration(page, duration: totalDuration) let sampleRate = 48000 let chimeBuffer = engine.editor.createBuffer() // Generate the chime samples using the WAV helper. let chimeWav = createWavData( sampleRate: sampleRate, durationSeconds: successChime.totalDuration, ) { time in var sample = 0.0 for note in successChime.notes { let envelope = adsr( time: time, noteStart: note.start, noteDuration: note.duration, attack: 0.02, // Soft attack (20ms) decay: 0.08, // Gentle decay (80ms) sustain: 0.7, // Sustain at 70% release: 0.25, // Smooth release (250ms) ) if envelope > 0 { // Sine wave with two harmonics for a richer tone. let fundamental = sin(2 * .pi * note.freq * time) let harmonic2 = sin(4 * .pi * note.freq * time) * 0.25 let harmonic3 = sin(6 * .pi * note.freq * time) * 0.1 sample += (fundamental + harmonic2 + harmonic3) * envelope * 0.3 } } return sample } try engine.editor.setBufferData(url: chimeBuffer, offset: 0, data: chimeWav) let chimeLength = try engine.editor.getBufferLength(url: chimeBuffer) let chimeBytes = try engine.editor.getBufferData( url: chimeBuffer, offset: 0, length: chimeLength.uintValue, ) _ = chimeBytes let chimeBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: chimeBlock) try engine.block.setURL(chimeBlock, property: "audio/fileURI", value: chimeBuffer) // Position the chime at the start of the timeline. try engine.block.setTimeOffset(chimeBlock, offset: 0) try engine.block.setDuration(chimeBlock, duration: successChime.totalDuration) try engine.block.setVolume(chimeBlock, volume: 0.8) let melodyBuffer = engine.editor.createBuffer() let melodyWav = createWavData( sampleRate: sampleRate, durationSeconds: notificationMelody.totalDuration, ) { time in var sample = 0.0 for note in notificationMelody.notes { let envelope = adsr( time: time, noteStart: note.start, noteDuration: note.duration, attack: 0.01, decay: 0.06, sustain: 0.6, release: 0.2, ) if envelope > 0 { let fundamental = sin(2 * .pi * note.freq * time) let harmonic2 = sin(4 * .pi * note.freq * time) * 0.15 sample += (fundamental + harmonic2) * envelope * 0.4 } } return sample } try engine.editor.setBufferData(url: melodyBuffer, offset: 0, data: melodyWav) let melodyBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: melodyBlock) try engine.block.setURL(melodyBlock, property: "audio/fileURI", value: melodyBuffer) try engine.block.setTimeOffset(melodyBlock, offset: effectDuration + gapDuration) // 2.5s try engine.block.setDuration(melodyBlock, duration: notificationMelody.totalDuration) try engine.block.setVolume(melodyBlock, volume: 0.8) let alertBuffer = engine.editor.createBuffer() let alertWav = createWavData( sampleRate: sampleRate, durationSeconds: alertTone.totalDuration, ) { time in var sample = 0.0 for note in alertTone.notes { let envelope = adsr( time: time, noteStart: note.start, noteDuration: note.duration, attack: 0.005, decay: 0.05, sustain: 0.5, release: 0.15, ) if envelope > 0 { let fundamental = sin(2 * .pi * note.freq * time) let harmonic2 = sin(4 * .pi * note.freq * time) * 0.2 let harmonic3 = sin(6 * .pi * note.freq * time) * 0.15 sample += (fundamental + harmonic2 + harmonic3) * envelope * 0.35 } } return sample } try engine.editor.setBufferData(url: alertBuffer, offset: 0, data: alertWav) let alertBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: alertBlock) try engine.block.setURL(alertBlock, property: "audio/fileURI", value: alertBuffer) try engine.block.setTimeOffset(alertBlock, offset: 2 * (effectDuration + gapDuration)) // 5s try engine.block.setDuration(alertBlock, duration: alertTone.totalDuration) try engine.block.setVolume(alertBlock, volume: 0.75) let archive = try await engine.scene.saveToArchive() _ = archive } ``` This guide covers working with buffers to create audio data and position it in the composition. ## Working with Buffers CE.SDK provides a buffer API for creating and managing arbitrary binary data in memory. Use buffers when you need to generate content programmatically rather than loading it from a file. ### Creating a Buffer Create a buffer with `engine.editor.createBuffer()`, which returns a `URL` you can use to reference the buffer: ```swift highlight-addSoundEffects-bufferCreate let chimeBuffer = engine.editor.createBuffer() ``` ### Writing Data Write data to a buffer using `engine.editor.setBufferData(url:offset:data:)`. The `offset` parameter specifies where to start writing: ```swift highlight-addSoundEffects-bufferWrite try engine.editor.setBufferData(url: chimeBuffer, offset: 0, data: chimeWav) ``` ### Reading Data Read data back with `engine.editor.getBufferData(url:offset:length:)`, which returns the raw `Data`. Query the current size with `engine.editor.getBufferLength(url:)`, which returns the byte count as an `NSNumber`: ```swift highlight-addSoundEffects-bufferRead let chimeLength = try engine.editor.getBufferLength(url: chimeBuffer) let chimeBytes = try engine.editor.getBufferData( url: chimeBuffer, offset: 0, length: chimeLength.uintValue, ) ``` ### Adding an Audio Track Create an audio block and link it to the buffer URL. The buffer's URL goes into the block's `audio/fileURI` property via `setURL(_:property:value:)`. Append the block to the page so it joins the composition: ```swift highlight-addSoundEffects-audioTrack let chimeBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: chimeBlock) try engine.block.setURL(chimeBlock, property: "audio/fileURI", value: chimeBuffer) ``` ### Cleanup Call `engine.editor.destroyBuffer(url:)` when a buffer is no longer needed. Buffers are also cleaned up automatically with the scene. ## Generating Audio Data To use a buffer for audio, you need valid audio data. WAV is a convenient choice because it's a 44-byte header followed by raw PCM samples — straightforward to build in memory: ```swift highlight-addSoundEffects-wavHelper func createWavData( sampleRate: Int, durationSeconds: Double, generator: (Double) -> Double, ) -> Data { let bitsPerSample: UInt16 = 16 let channels: UInt16 = 2 // Stereo output let numSamples = Int(durationSeconds * Double(sampleRate)) let dataSize = UInt32(numSamples * Int(channels) * Int(bitsPerSample / 8)) var data = Data(capacity: 44 + Int(dataSize)) func writeLE16(_ value: UInt16) { var le = value.littleEndian withUnsafeBytes(of: &le) { data.append(contentsOf: $0) } } func writeLE32(_ value: UInt32) { var le = value.littleEndian withUnsafeBytes(of: &le) { data.append(contentsOf: $0) } } func writeSample(_ value: Int16) { var le = value.littleEndian withUnsafeBytes(of: &le) { data.append(contentsOf: $0) } } // RIFF chunk descriptor data.append(contentsOf: [0x52, 0x49, 0x46, 0x46]) // "RIFF" writeLE32(36 + dataSize) // File size - 8 data.append(contentsOf: [0x57, 0x41, 0x56, 0x45]) // "WAVE" // fmt sub-chunk data.append(contentsOf: [0x66, 0x6D, 0x74, 0x20]) // "fmt " writeLE32(16) // Sub-chunk size (16 for PCM) writeLE16(1) // Audio format (1 = PCM) writeLE16(channels) writeLE32(UInt32(sampleRate)) writeLE32(UInt32(sampleRate) * UInt32(channels) * UInt32(bitsPerSample / 8)) writeLE16(channels * (bitsPerSample / 8)) // Block align writeLE16(bitsPerSample) // data sub-chunk data.append(contentsOf: [0x64, 0x61, 0x74, 0x61]) // "data" writeLE32(dataSize) // Generate audio samples — duplicate mono value to both stereo channels. for i in 0 ..< numSamples { let time = Double(i) / Double(sampleRate) let value = generator(time) let clamped = max(-1.0, min(1.0, value)) let sample = Int16((clamped * 32767.0).rounded()) writeSample(sample) // Left channel writeSample(sample) // Right channel } return data } ``` The helper writes the RIFF header, format chunk, and data chunk, then iterates through time to generate samples from a generator closure that returns values between `-1.0` and `1.0`. Each mono sample is duplicated into both channels for a stereo file. ## Creating Sound Effect Generators ### ADSR Envelope Shape notes with ADSR envelopes (attack, decay, sustain, release) to avoid clicks and create natural-sounding tones: ```swift highlight-addSoundEffects-envelopeHelper func adsr( time: Double, noteStart: Double, noteDuration: Double, attack: Double, decay: Double, sustain: Double, release: Double, ) -> Double { let t = time - noteStart guard t >= 0 else { return 0 } let noteEnd = noteDuration - release if t < attack { // Attack phase: ramp up from 0 to 1 return t / attack } else if t < attack + decay { // Decay phase: ramp down from 1 to sustain level return 1 - ((t - attack) / decay) * (1 - sustain) } else if t < noteEnd { // Sustain phase: hold at sustain level return sustain } else if t < noteDuration { // Release phase: ramp down from sustain to 0 return sustain * (1 - (t - noteEnd) / release) } return 0 } ``` The envelope shapes volume over time — quickly ramping up during attack, gradually falling during decay, holding steady during sustain, and fading out during release. ### Sound Effect Definitions Define sound effects as note sequences with frequencies, start times, and durations: ```swift highlight-addSoundEffects-soundDefinitions struct Note { let freq: Double let start: Double let duration: Double } struct SoundEffect { let notes: [Note] let totalDuration: Double } // Musical note frequencies (Hz) for the 4th and 5th octaves. enum Notes { static let c4 = 261.63 static let e4 = 329.63 static let g4 = 392.0 static let a4 = 440.0 static let c5 = 523.25 static let d5 = 587.33 static let e5 = 659.25 static let f5 = 698.46 static let g5 = 783.99 static let a5 = 880.0 } // Sound effect 1: Ascending "success" fanfare with overlapping arpeggio and chord. let successChime = SoundEffect( notes: [ Note(freq: Notes.c4, start: 0.0, duration: 0.4), Note(freq: Notes.e4, start: 0.1, duration: 0.4), Note(freq: Notes.g4, start: 0.2, duration: 0.5), Note(freq: Notes.c5, start: 0.35, duration: 1.65), Note(freq: Notes.e5, start: 0.4, duration: 1.6), Note(freq: Notes.g5, start: 0.45, duration: 1.55), ], totalDuration: 2.0, ) // Sound effect 2: Gentle notification melody that resolves pleasantly. let notificationMelody = SoundEffect( notes: [ Note(freq: Notes.e5, start: 0.0, duration: 0.4), Note(freq: Notes.g5, start: 0.25, duration: 0.5), Note(freq: Notes.a5, start: 0.6, duration: 0.3), Note(freq: Notes.g5, start: 0.85, duration: 0.4), Note(freq: Notes.e5, start: 1.15, duration: 0.85), ], totalDuration: 2.0, ) // Sound effect 3: Descending alert tone that grabs attention. let alertTone = SoundEffect( notes: [ Note(freq: Notes.a5, start: 0.0, duration: 0.25), Note(freq: Notes.a5, start: 0.3, duration: 0.25), Note(freq: Notes.f5, start: 0.6, duration: 0.4), Note(freq: Notes.d5, start: 0.9, duration: 0.5), Note(freq: Notes.a4, start: 1.3, duration: 0.7), ], totalDuration: 2.0, ) ``` Each sound effect specifies a series of notes with their musical frequencies, when they start, and how long they play. Overlapping notes create chords and harmonic textures. ## Setting Up the Scene Audio blocks live on a timeline, so create a scene with a page sized to your output and set a total duration covering all sound effects: ```swift highlight-addSoundEffects-setup let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) // Total duration: 3 effects × 2s + 2 gaps × 0.5s = 7s let effectDuration = 2.0 let gapDuration = 0.5 let totalDuration = 3 * effectDuration + 2 * gapDuration try engine.block.setDuration(page, duration: totalDuration) let sampleRate = 48000 ``` ## Creating a Sound Effect Combine the buffer API with the WAV helper to build a complete sound effect. This example generates a notification melody by mixing multiple notes with harmonics: ```swift highlight-addSoundEffects-generateMelody let melodyBuffer = engine.editor.createBuffer() let melodyWav = createWavData( sampleRate: sampleRate, durationSeconds: notificationMelody.totalDuration, ) { time in var sample = 0.0 for note in notificationMelody.notes { let envelope = adsr( time: time, noteStart: note.start, noteDuration: note.duration, attack: 0.01, decay: 0.06, sustain: 0.6, release: 0.2, ) if envelope > 0 { let fundamental = sin(2 * .pi * note.freq * time) let harmonic2 = sin(4 * .pi * note.freq * time) * 0.15 sample += (fundamental + harmonic2) * envelope * 0.4 } } return sample } try engine.editor.setBufferData(url: melodyBuffer, offset: 0, data: melodyWav) let melodyBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: melodyBlock) try engine.block.setURL(melodyBlock, property: "audio/fileURI", value: melodyBuffer) try engine.block.setTimeOffset(melodyBlock, offset: effectDuration + gapDuration) // 2.5s try engine.block.setDuration(melodyBlock, duration: notificationMelody.totalDuration) try engine.block.setVolume(melodyBlock, volume: 0.8) ``` The generator closure mixes overlapping notes, each with its own start time and duration. The `adsr` function shapes each note's volume over time, preventing harsh clicks. Adding a second harmonic at 15% creates a warmer tone than a pure sine wave. ## Positioning in Time Position audio blocks with `setTimeOffset(_:offset:)` (when the block starts) and `setDuration(_:duration:)` (how long it plays): ```swift highlight-addSoundEffects-timelinePosition // Position the chime at the start of the timeline. try engine.block.setTimeOffset(chimeBlock, offset: 0) try engine.block.setDuration(chimeBlock, duration: successChime.totalDuration) try engine.block.setVolume(chimeBlock, volume: 0.8) ``` ### Timeline Layout Example The example spaces three sound effects with 0.5-second gaps: ``` Timeline: |----|----|----|----|----|----|----| 0s 1s 2s 3s 4s 5s 6s 7s Success: |====| ^ 0s (2s) Melody: |====| ^ 2.5s (2s) Alert: |====| ^ 5s (2s) ``` Each effect is 2 seconds with 0.5-second gaps between them, for a total duration of 7 seconds. The third effect — an attention-grabbing alert tone — uses sharper attack and brighter harmonics: ```swift highlight-addSoundEffects-generateAlert let alertBuffer = engine.editor.createBuffer() let alertWav = createWavData( sampleRate: sampleRate, durationSeconds: alertTone.totalDuration, ) { time in var sample = 0.0 for note in alertTone.notes { let envelope = adsr( time: time, noteStart: note.start, noteDuration: note.duration, attack: 0.005, decay: 0.05, sustain: 0.5, release: 0.15, ) if envelope > 0 { let fundamental = sin(2 * .pi * note.freq * time) let harmonic2 = sin(4 * .pi * note.freq * time) * 0.2 let harmonic3 = sin(6 * .pi * note.freq * time) * 0.15 sample += (fundamental + harmonic2 + harmonic3) * envelope * 0.35 } } return sample } try engine.editor.setBufferData(url: alertBuffer, offset: 0, data: alertWav) let alertBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: alertBlock) try engine.block.setURL(alertBlock, property: "audio/fileURI", value: alertBuffer) try engine.block.setTimeOffset(alertBlock, offset: 2 * (effectDuration + gapDuration)) // 5s try engine.block.setDuration(alertBlock, duration: alertTone.totalDuration) try engine.block.setVolume(alertBlock, volume: 0.75) ``` ## Exporting the Scene Export the scene as an archive containing all audio data. `engine.scene.saveToArchive()` packages the scene with all embedded resources, including buffer data: ```swift highlight-addSoundEffects-export let archive = try await engine.scene.saveToArchive() ``` > **Note:** Buffer URLs are in-memory resources and cannot be serialized with `engine.scene.saveToString()`. Use `engine.scene.saveToArchive()` to export the complete scene with embedded audio buffers. ## Troubleshooting ### No Sound - **Check scene setup** — Ensure the audio block is attached to a page in the scene. - **Verify duration** — The audio block's duration must be greater than 0. - **Check buffer data** — The buffer must contain valid WAV data. ### Audio Sounds Wrong - **Clipping** — Clamp sample values to the `-1.0` to `1.0` range before conversion. - **Clicking** — Add attack and release phases to the envelope to avoid pops. - **Wrong pitch** — Verify frequency calculations and the sample rate (48 kHz). ### Buffer Errors - **Invalid WAV** — Ensure the header size fields match the actual data size. - **Format mismatch** — Use 16-bit PCM, stereo, 48 kHz for best compatibility. ## API Reference | Method | Description | | ----------------------------------------------------------------- | ------------------------------------------------ | | `engine.editor.createBuffer() -> URL` | Create a new buffer resource for arbitrary data. | | `engine.editor.setBufferData(url:offset:data:)` | Write data into a buffer. | | `engine.editor.getBufferLength(url:)` | Get the current length of a buffer in bytes. | | `engine.editor.getBufferData(url:offset:length:)` | Read data back from a buffer. | | `engine.editor.setBufferLength(url:length:)` | Resize a buffer. | | `engine.editor.destroyBuffer(url:)` | Free a buffer's resources. | | `engine.block.create(.audio)` | Create a new audio block. | | `engine.block.setURL(_:property:value:)` | Set a `URL`-valued property (e.g. `audio/fileURI`). | | `engine.block.setTimeOffset(_:offset:)` | Set when the audio block starts on the timeline. | | `engine.block.setDuration(_:duration:)` | Set the duration of the audio block. | | `engine.block.setVolume(_:volume:)` | Set the volume level (`0.0` to `1.0`). | | `engine.block.appendChild(to:child:)` | Attach the audio block to a page. | | `engine.scene.saveToArchive()` | Export the scene with all embedded resources. | ## Next Steps - [Add Music](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-music-5b182c/) — Add background music and audio tracks to video projects - [Adjust Audio Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/) — Fine-tune audio levels and balance multiple sources - [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-speed-908d57/) — Create slow-motion, time-stretched, and fast-forward audio effects - [Loop Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/loop-937be7/) — Loop audio tracks for continuous playback - [Record Voiceover](#broken-link-07e8e1) — On iOS, let users capture voiceover clips directly in the editor UI - [Trim](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) — Control media playback timing --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Adjust Audio Playback Speed" description: "Control audio playback speed from quarter-speed (0.25x) to triple-speed (3.0x) using the CE.SDK engine API." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-speed-908d57/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio-2f700b/) > [Adjust Speed](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-speed-908d57/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-audio-adjust-speed/CreateAudioAdjustSpeed.swift reference-only import Foundation import IMGLYEngine @MainActor func createAudioAdjustSpeed(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) let baseURL = try engine.guidesBaseURL let audioBlock = try engine.block.create(.audio) try engine.block.setURL( audioBlock, property: "audio/fileURI", value: baseURL.appendingPathComponent("ly.img.audio/audios/far_from_home.m4a"), ) // Wait for the audio resource to load so duration and speed APIs work correctly. try await engine.block.forceLoadAVResource(audioBlock) // Slow Motion Audio (0.5x — half speed, doubles duration). let slowAudioBlock = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: slowAudioBlock) try engine.block.setTimeOffset(slowAudioBlock, offset: 0) try engine.block.setPlaybackSpeed(slowAudioBlock, speed: 0.5) // Normal Speed Audio (1.0x — original playback rate). let normalAudioBlock = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: normalAudioBlock) try engine.block.setTimeOffset(normalAudioBlock, offset: 5) try engine.block.setPlaybackSpeed(normalAudioBlock, speed: 1.0) // Query current speed to verify the change. let currentSpeed = try engine.block.getPlaybackSpeed(normalAudioBlock) print("Normal speed block set to: \(currentSpeed)x") // Maximum Speed Audio (3.0x — triple speed, reduces duration to 1/3). let maxSpeedAudioBlock = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: maxSpeedAudioBlock) try engine.block.setTimeOffset(maxSpeedAudioBlock, offset: 10) try engine.block.setPlaybackSpeed(maxSpeedAudioBlock, speed: 3.0) // Log duration changes to demonstrate the speed-duration relationship. let slowDuration = try engine.block.getDuration(slowAudioBlock) let normalDuration = try engine.block.getDuration(normalAudioBlock) let maxDuration = try engine.block.getDuration(maxSpeedAudioBlock) print(String(format: "Slow motion (0.5x) duration: %.2fs", slowDuration)) print(String(format: "Normal speed (1.0x) duration: %.2fs", normalDuration)) print(String(format: "Maximum speed (3.0x) duration: %.2fs", maxDuration)) // Remove the original audio block (we only need the duplicates). try engine.block.destroy(audioBlock) let sceneContent = try await engine.scene.saveToString() _ = sceneContent } ``` Control audio playback speed programmatically using CE.SDK's headless engine, from quarter-speed (0.25x) to triple-speed (3.0x). > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-audio-adjust-speed) Playback speed adjustment changes how fast or slow audio plays. A speed multiplier of 1.0 represents normal speed, values below 1.0 slow down playback, and values above 1.0 speed it up. This technique is commonly used for podcast speed controls, time-compressed narration, slow-motion audio effects, and accessibility features. This guide covers how to adjust audio playback speed programmatically using the Engine API, understand speed constraints, and manage how speed changes affect block duration. ## Understanding Speed Concepts CE.SDK supports playback speeds from **0.25x** (quarter speed) to **3.0x** (triple speed), with **1.0x** as the default normal speed. Values below 1.0 slow down playback, values above 1.0 speed it up. **Speed and Duration**: Adjusting speed automatically changes the block's duration following an inverse relationship: `perceived_duration = original_duration / speed_multiplier`. A 10-second clip at 2.0x speed plays in 5 seconds; at 0.5x speed it takes 20 seconds. This automatic adjustment maintains synchronization when coordinating audio with other elements. **Common use cases**: Podcast playback controls (1.5x–2.0x), accessibility features (0.75x for easier comprehension), time-compressed narration, dramatic slow-motion effects (0.25x–0.5x), transcription work, and music tempo adjustments. ## Setting Up the Engine Audio blocks require a scene with timeline support. Create a video scene and add a page to host the audio blocks. ```swift highlight-createAudioAdjustSpeed-setup let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) ``` ## Setting Up Audio for Speed Adjustment ### Loading Audio Files Create an audio block and load an audio file by setting its `audio/fileURI` property. ```swift highlight-createAudioAdjustSpeed-createAudio let audioBlock = try engine.block.create(.audio) try engine.block.setURL( audioBlock, property: "audio/fileURI", value: baseURL.appendingPathComponent("ly.img.audio/audios/far_from_home.m4a"), ) // Wait for the audio resource to load so duration and speed APIs work correctly. try await engine.block.forceLoadAVResource(audioBlock) ``` Unlike video or image blocks that use fills, audio blocks store the file URI directly on the block itself via the `audio/fileURI` property. Awaiting `forceLoadAVResource` ensures CE.SDK has downloaded the audio file and loaded its metadata, which is essential for accurate duration information and playback speed control. ## Adjusting Playback Speed ### Setting Normal Speed By default, audio plays at normal speed (1.0x). Set it explicitly to ensure consistent baseline behavior, and call `getPlaybackSpeed` to read the current multiplier. ```swift highlight-createAudioAdjustSpeed-setNormalSpeed // Normal Speed Audio (1.0x — original playback rate). let normalAudioBlock = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: normalAudioBlock) try engine.block.setTimeOffset(normalAudioBlock, offset: 5) try engine.block.setPlaybackSpeed(normalAudioBlock, speed: 1.0) // Query current speed to verify the change. let currentSpeed = try engine.block.getPlaybackSpeed(normalAudioBlock) print("Normal speed block set to: \(currentSpeed)x") ``` Setting speed to 1.0 ensures the audio plays at its original recorded rate. This is useful after experimenting with different speeds and wanting to return to normal, or when initializing audio blocks programmatically to ensure consistent starting states. Reading back the current speed is handy for populating UI controls or validating relative adjustments. ## Common Speed Presets ### Slow Motion Audio (0.5x) Slowing audio to half speed creates a slow-motion effect that's useful for careful listening or transcription. ```swift highlight-createAudioAdjustSpeed-setSlowMotion // Slow Motion Audio (0.5x — half speed, doubles duration). let slowAudioBlock = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: slowAudioBlock) try engine.block.setTimeOffset(slowAudioBlock, offset: 0) try engine.block.setPlaybackSpeed(slowAudioBlock, speed: 0.5) ``` At 0.5x speed, a 10-second audio clip will take 20 seconds to play. This slower pace makes it easier to catch details, transcribe speech accurately, or create dramatic slow-motion audio effects in creative projects. ### Maximum Speed (3.0x) The maximum supported speed is 3.0x, three times normal playback rate. ```swift highlight-createAudioAdjustSpeed-setMaximumSpeed // Maximum Speed Audio (3.0x — triple speed, reduces duration to 1/3). let maxSpeedAudioBlock = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: maxSpeedAudioBlock) try engine.block.setTimeOffset(maxSpeedAudioBlock, offset: 10) try engine.block.setPlaybackSpeed(maxSpeedAudioBlock, speed: 3.0) ``` At maximum speed, audio plays very quickly — a 10-second clip finishes in just 3.33 seconds. This extreme speed is useful for rapidly skimming through content to find specific moments, though comprehension becomes challenging at this rate. ## Speed and Block Duration ### Understanding Duration Changes When you change playback speed, CE.SDK automatically adjusts the block's duration to reflect the new playback time. ```swift highlight-createAudioAdjustSpeed-speedAndDuration // Log duration changes to demonstrate the speed-duration relationship. let slowDuration = try engine.block.getDuration(slowAudioBlock) let normalDuration = try engine.block.getDuration(normalAudioBlock) let maxDuration = try engine.block.getDuration(maxSpeedAudioBlock) print(String(format: "Slow motion (0.5x) duration: %.2fs", slowDuration)) print(String(format: "Normal speed (1.0x) duration: %.2fs", normalDuration)) print(String(format: "Maximum speed (3.0x) duration: %.2fs", maxDuration)) ``` The logged durations illustrate the inverse relationship: the 0.5x block reports roughly double the source duration, the 1.0x block reports the source duration, and the 3.0x block reports about one-third. The audio content is identical across all three blocks — only the playback rate differs, and CE.SDK shrinks or extends the block's duration accordingly so the timeline stays synchronized. ## Exporting Results After adjusting audio speeds, serialize the scene to preserve your work. `engine.scene.saveToString()` captures the entire scene, including every audio block with its speed setting. ```swift highlight-createAudioAdjustSpeed-export let sceneContent = try await engine.scene.saveToString() ``` The returned `.scene` string can be loaded later for further editing or used as a template for batch processing workflows. ## API Reference | Method | Description | Parameters | Returns | | --- | --- | --- | --- | | `engine.block.create(.audio)` | Creates a new audio block | — | `DesignBlockID` | | `engine.block.setString(_:property:value:)` | Sets a string property on a block | `id: DesignBlockID, property: String, value: String` | `Void` | | `engine.block.forceLoadAVResource(_:)` | Forces loading of audio resource metadata | `id: DesignBlockID` | `async throws` | | `engine.block.duplicate(_:attachToParent:)` | Duplicates a block including its children | `id: DesignBlockID, attachToParent: Bool = true` | `DesignBlockID` | | `engine.block.setTimeOffset(_:offset:)` | Sets the block's time offset on the timeline | `id: DesignBlockID, offset: Double` | `Void` | | `engine.block.setPlaybackSpeed(_:speed:)` | Sets the playback speed multiplier. Valid range is \[0.25, 3.0] for audio blocks. Also adjusts duration | `id: DesignBlockID, speed: Float` | `Void` | | `engine.block.getPlaybackSpeed(_:)` | Gets the current playback speed multiplier | `id: DesignBlockID` | `Float` | | `engine.block.getDuration(_:)` | Gets the playback duration of a block in seconds | `id: DesignBlockID` | `Double` | | `engine.block.destroy(_:)` | Destroys a block | `id: DesignBlockID` | `Void` | | `engine.scene.saveToString()` | Serializes the current scene into a string | — | `async throws -> String` | ## Next Steps - [Add Music](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-music-5b182c/) — Add background music and audio tracks to video projects - [Adjust Audio Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/) — Control audio playback levels and balance multiple audio sources - [Add Sound Effects](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-sound-effects-9e984e/) — Generate sound effects programmatically from raw PCM data using audio buffers - [Loop Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/loop-937be7/) — Loop audio tracks for continuous playback - [Record Voiceover](#broken-link-07e8e1) — On iOS, let users capture voiceover clips directly in the editor UI --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Adjust Audio Volume" description: "Learn how to adjust audio volume in CE.SDK Engine for Swift to control playback levels, mute audio, and balance multiple audio sources in video projects." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio-2f700b/) > [Adjust Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/) --- Control audio playback volume using CE.SDK's Engine API for Swift, from silent (0.0) to full volume (1.0). > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-audio-audio-adjust-volume) Volume control adjusts how loud or quiet audio plays during playback. CE.SDK uses a normalized 0.0–1.0 range where 0.0 is completely silent and 1.0 is full volume. This applies to both audio blocks and video fills with embedded audio. Volume settings are commonly used for balancing multiple audio sources, creating fade effects, and allowing users to adjust playback levels. ```swift file=@cesdk_swift_examples/engine-guides-create-audio-audio-adjust-volume/AdjustVolume.swift reference-only import Foundation import IMGLYEngine @MainActor func adjustVolume(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 20) let baseURL = try engine.guidesBaseURL let audioURL = baseURL.appendingPathComponent("ly.img.audio/audios/dance_harder.m4a") // Create an audio block and load the audio file. let audioBlock = try engine.block.create(.audio) try engine.block.setURL(audioBlock, property: "audio/fileURI", value: audioURL) // Wait for the audio resource to load before adjusting volume or querying state. try await engine.block.forceLoadAVResource(audioBlock) // Set volume to 80% (0.8 on a 0.0-1.0 scale). let fullVolumeAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: fullVolumeAudio) try engine.block.setTimeOffset(fullVolumeAudio, offset: 0) try engine.block.setVolume(fullVolumeAudio, volume: 0.8) // Set volume to 30% for background music. let lowVolumeAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: lowVolumeAudio) try engine.block.setTimeOffset(lowVolumeAudio, offset: 5) try engine.block.setVolume(lowVolumeAudio, volume: 0.3) // Mute an audio block. The volume setting is preserved so unmuting restores playback at the same level. let mutedAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: mutedAudio) try engine.block.setTimeOffset(mutedAudio, offset: 10) try engine.block.setVolume(mutedAudio, volume: 1.0) try engine.block.setMuted(mutedAudio, muted: true) // Query current volume and mute states. let currentVolume = try engine.block.getVolume(fullVolumeAudio) let lowVolume = try engine.block.getVolume(lowVolumeAudio) let isMuted = try engine.block.isMuted(mutedAudio) let isForceMuted = try engine.block.isForceMuted(mutedAudio) print(String(format: "Full volume audio: %.0f%%", currentVolume * 100)) print(String(format: "Low volume audio: %.0f%%", lowVolume * 100)) print("Muted audio — isMuted: \(isMuted), isForceMuted: \(isForceMuted)") // Map a slider value (0-100) to the normalized 0.0-1.0 volume range. let sliderValue: Float = 75 let volume = sliderValue / 100 try engine.block.setVolume(fullVolumeAudio, volume: volume) // Toggle mute state and react to a force-muted block (e.g. video fill playing above 3.0x). let currentlyMuted = try engine.block.isMuted(mutedAudio) try engine.block.setMuted(mutedAudio, muted: !currentlyMuted) if try engine.block.isForceMuted(mutedAudio) { // Show a distinct "force muted" indicator in the UI. } // Remove the original audio block; only the duplicates are part of the scene. try engine.block.destroy(audioBlock) } ``` This guide covers how to adjust audio volume programmatically using the Engine API, mute and unmute audio, and query volume and mute states. ## Understanding Volume Concepts CE.SDK supports volume levels from **0.0** (silent) to **1.0** (full volume), with **1.0** as the default for new audio blocks. Values in between represent proportional volume levels — 0.5 is half volume, 0.25 is quarter volume. **Volume vs muting**: Setting volume to 0.0 makes audio silent, but `setMuted(_:muted:)` is preferred when you want to temporarily silence audio without losing the volume setting. Unmuting restores the previous volume level. **Common use cases**: background music mixing (0.3–0.5 under voiceover), user volume controls, audio balancing for multi-track projects, fade effects (gradually adjusting volume over time), and accessibility features. ## Setting Up Audio for Volume Control ### Loading Audio Files Create an audio block and load an audio file by setting its `audio/fileURI` property. ```swift highlight-adjustVolume-create-audio // Create an audio block and load the audio file. let audioBlock = try engine.block.create(.audio) try engine.block.setURL(audioBlock, property: "audio/fileURI", value: audioURL) // Wait for the audio resource to load before adjusting volume or querying state. try await engine.block.forceLoadAVResource(audioBlock) ``` Unlike video or image blocks which use fills, audio blocks store the file URI directly on the block itself via the `audio/fileURI` property. The `forceLoadAVResource(_:)` call ensures CE.SDK has downloaded the audio file and loaded its metadata before the block is manipulated. ## Adjusting Volume ### Setting Volume Set volume using `setVolume(_:volume:)` with a `Float` value between `0.0` and `1.0`. ```swift highlight-adjustVolume-set-volume // Set volume to 80% (0.8 on a 0.0-1.0 scale). let fullVolumeAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: fullVolumeAudio) try engine.block.setTimeOffset(fullVolumeAudio, offset: 0) try engine.block.setVolume(fullVolumeAudio, volume: 0.8) ``` Setting volume to 0.8 (80%) is useful when you want prominent audio that isn't at maximum level, leaving headroom for other audio sources or preventing distortion. ### Setting Low Volume for Background Audio For background music that should be audible but not prominent, use lower volume levels. ```swift highlight-adjustVolume-set-low-volume // Set volume to 30% for background music. let lowVolumeAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: lowVolumeAudio) try engine.block.setTimeOffset(lowVolumeAudio, offset: 5) try engine.block.setVolume(lowVolumeAudio, volume: 0.3) ``` At 0.3 (30%) volume, the audio is clearly audible but stays in the background. This is a common level for background music under voiceover or dialogue. ## Muting Audio ### Mute and Unmute Use `setMuted(_:muted:)` to mute audio without changing its volume setting. This is useful for toggle controls. ```swift highlight-adjustVolume-mute-audio // Mute an audio block. The volume setting is preserved so unmuting restores playback at the same level. let mutedAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: mutedAudio) try engine.block.setTimeOffset(mutedAudio, offset: 10) try engine.block.setVolume(mutedAudio, volume: 1.0) try engine.block.setMuted(mutedAudio, muted: true) ``` When an audio block is muted, the volume setting (1.0 in this case) is preserved. Unmuting later with `setMuted(block, muted: false)` restores playback at the same volume level. ### Querying Volume and Mute States Query current volume and mute states at any time. ```swift highlight-adjustVolume-query-volume // Query current volume and mute states. let currentVolume = try engine.block.getVolume(fullVolumeAudio) let lowVolume = try engine.block.getVolume(lowVolumeAudio) let isMuted = try engine.block.isMuted(mutedAudio) let isForceMuted = try engine.block.isForceMuted(mutedAudio) print(String(format: "Full volume audio: %.0f%%", currentVolume * 100)) print(String(format: "Low volume audio: %.0f%%", lowVolume * 100)) print("Muted audio — isMuted: \(isMuted), isForceMuted: \(isForceMuted)") ``` Use `getVolume(_:)` to read the current volume level, `isMuted(_:)` to check if the block is muted by the user, and `isForceMuted(_:)` to check if the engine has automatically muted the block due to playback rules. ## Mixing Multiple Audio Sources ### Balancing Tracks When working with multiple audio sources, use different volume levels to create a balanced mix. A common approach is to keep voiceover or dialogue at higher levels (0.8–1.0) and background music at lower levels (0.3–0.5). ### Common Mixing Patterns **Voiceover prominent**: set background music to 0.3 and voiceover to 1.0 for clear narration with musical accompaniment. **Balanced dialogue and music**: set both to 0.6–0.7 when both elements are equally important. **Sound effects as accents**: set sound effects to 0.5–0.8 depending on how prominent they should be in the mix. ## Building Volume Controls ### Volume Slider When building a volume slider, map the slider value directly to the 0.0–1.0 range. Display percentages (0–100%) for user-friendly labels. ```swift highlight-adjustVolume-volume-slider // Map a slider value (0-100) to the normalized 0.0-1.0 volume range. let sliderValue: Float = 75 let volume = sliderValue / 100 try engine.block.setVolume(fullVolumeAudio, volume: volume) ``` ### Mute Toggle Implement mute buttons using `setMuted(_:muted:)` and indicate the current state using `isMuted(_:)`. Show a different icon when `isForceMuted(_:)` returns `true` to indicate the engine has automatically muted the audio. ```swift highlight-adjustVolume-mute-toggle // Toggle mute state and react to a force-muted block (e.g. video fill playing above 3.0x). let currentlyMuted = try engine.block.isMuted(mutedAudio) try engine.block.setMuted(mutedAudio, muted: !currentlyMuted) if try engine.block.isForceMuted(mutedAudio) { // Show a distinct "force muted" indicator in the UI. } ``` ## Troubleshooting ### Volume Changes Not Audible Check if the block is muted with `isMuted(_:)` or force muted with `isForceMuted(_:)`. Verify the audio resource has loaded successfully via `forceLoadAVResource(_:)`. ### Force Muted State Video fills at playback speeds above 3.0x are automatically force muted by the engine. Reduce the playback speed to restore audio output. ### Volume Not Persisting Ensure you're setting volume on the correct block ID. Volume settings are block-specific and don't propagate to duplicates or other instances. ## API Reference | Method | Category | Purpose | | ---------------------------- | -------- | ----------------------------------------- | | `block.setVolume(_:volume:)` | Block | Set volume level (0.0 to 1.0) | | `block.getVolume(_:)` | Block | Get current volume level | | `block.setMuted(_:muted:)` | Block | Mute or unmute audio | | `block.isMuted(_:)` | Block | Check if audio is muted | | `block.isForceMuted(_:)` | Block | Check if the engine has force muted audio | ## Next Steps - [Add Music](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-music-5b182c/) — Add background music and audio tracks - [Add Sound Effects](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-sound-effects-9e984e/) — Generate sound effects programmatically from raw PCM data using audio buffers - [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-speed-908d57/) — Create slow-motion, time-stretched, and fast-forward audio effects - [Loop Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/loop-937be7/) — Loop audio tracks for continuous playback - [Record Voiceover](#broken-link-07e8e1) — On iOS, let users capture voiceover clips directly in the editor UI --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Fade Audio In and Out" description: "Learn how to fade audio in and out in CE.SDK Engine for Swift, choose an easing curve, read the fade configuration back, and fade the audio of video fills." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/fade-a4d7e2/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio-2f700b/) > [Fade In and Out](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/fade-a4d7e2/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-audio-audio-fade/Fade.swift reference-only import Foundation import IMGLYEngine @MainActor func fadeAudio(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 30) let baseURL = try engine.guidesBaseURL let audioURL = baseURL.appendingPathComponent("ly.img.audio/audios/dance_harder.m4a") let videoURL = baseURL.appendingPathComponent("ly.img.video/videos/pexels-kampus-production-8154913.mp4") // Create an audio block, load its resource, and give it a duration on the timeline. let audioBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: audioBlock) try engine.block.setURL(audioBlock, property: "audio/fileURI", value: audioURL) try await engine.block.forceLoadAVResource(audioBlock) try engine.block.setTimeOffset(audioBlock, offset: 0) try engine.block.setDuration(audioBlock, duration: 12) try engine.block.setVolume(audioBlock, volume: 0.8) // Ramp up from silence over the first 3 seconds of the clip. try engine.block.setAudioFadeIn(audioBlock, duration: 3.0) // Ramp down to silence over the last 2 seconds of the clip. try engine.block.setAudioFadeOut(audioBlock, duration: 2.0) // A second clip that eases in and out instead of ramping linearly. let easedAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: easedAudio) try engine.block.setTimeOffset(easedAudio, offset: 14) try engine.block.setDuration(easedAudio, duration: 12) try engine.block.setAudioFadeIn(easedAudio, duration: 3.0, easing: .easeInOut) try engine.block.setAudioFadeOut(easedAudio, duration: 3.0, easing: .easeOut) // Build a video block whose fill carries the embedded audio. let videoBlock = try engine.block.create(.graphic) try engine.block.setShape(videoBlock, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(videoBlock, fill: videoFill) try engine.block.appendChild(to: page, child: videoBlock) try engine.block.fillParent(videoBlock) try await engine.block.forceLoadAVResource(videoFill) try engine.block.setDuration(videoBlock, duration: 10) // Video audio lives on the video fill, so resolve the fill first — exactly as with `setVolume`. let fill = try engine.block.getFill(videoBlock) try engine.block.setAudioFadeIn(fill, duration: 1.5) try engine.block.setAudioFadeOut(fill, duration: 1.5, easing: .easeOut) // Read the configuration back through the block properties to drive UI controls. let fadeInDuration = try engine.block.getDouble(audioBlock, property: "playback/fadeIn/duration") let fadeInEasing = try engine.block.getEnum(audioBlock, property: "playback/fadeIn/easing") let fadeOutDuration = try engine.block.getDouble(audioBlock, property: "playback/fadeOut/duration") let fadeOutEasing = try engine.block.getEnum(audioBlock, property: "playback/fadeOut/easing") print("Fade in: \(fadeInDuration)s (\(fadeInEasing))") print("Fade out: \(fadeOutDuration)s (\(fadeOutEasing))") // A duration of 0 removes a fade again. try engine.block.setAudioFadeOut(audioBlock, duration: 0) let removedFadeOut = try engine.block.getDouble(audioBlock, property: "playback/fadeOut/duration") print("Fade out after removal: \(removedFadeOut)s") } ``` Ramp audio up at the start of a clip and down at the end using CE.SDK's Engine API for Swift, with a duration in seconds and an optional easing curve. > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-audio-audio-fade) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. An audio fade ramps the volume of a clip between silence and its configured volume over a fixed duration. Use `setAudioFadeIn(_:duration:easing:)` for the start of a clip and `setAudioFadeOut(_:duration:easing:)` for the end. Both take a duration in seconds and an optional easing curve, and a duration of `0` — the default — means no fade. ## Understanding Audio Fades ### How the Fade Gain Is Applied The fade produces a gain between `0.0` and `1.0` that is multiplied with the block's volume. A fade-in on a block at 50% volume therefore ramps from silence to 50%, not to 100%. Fades combine with page volume, muting, and clip transitions the same way. ### Fades Are Anchored to the Timeline The fade-in window starts at the beginning of the block and the fade-out window ends at the end of the block, measured against the block's timeline duration. Trimming or resizing a clip keeps the fade-in at the audible start and the fade-out at the audible end. ## Setting Up an Audio Clip Create an audio block, load its resource, and place it on the timeline. The `forceLoadAVResource(_:)` call ensures the audio file and its metadata are available before the block is configured. ```swift highlight-fadeAudio-create-audio // Create an audio block, load its resource, and give it a duration on the timeline. let audioBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: audioBlock) try engine.block.setURL(audioBlock, property: "audio/fileURI", value: audioURL) try await engine.block.forceLoadAVResource(audioBlock) try engine.block.setTimeOffset(audioBlock, offset: 0) try engine.block.setDuration(audioBlock, duration: 12) try engine.block.setVolume(audioBlock, volume: 0.8) ``` Fades apply only to audio blocks and video fills. Passing any other block — including a page or the graphic block that owns a video fill — throws. ## Fading Audio In Set a fade-in with `setAudioFadeIn(_:duration:easing:)`. The audio ramps up from silence over the given number of seconds at the start of the block. ```swift highlight-fadeAudio-fade-in // Ramp up from silence over the first 3 seconds of the clip. try engine.block.setAudioFadeIn(audioBlock, duration: 3.0) ``` Negative and `NaN` durations are clamped to `0`, which disables the fade. A positive infinite duration throws an error. ## Fading Audio Out Set a fade-out with `setAudioFadeOut(_:duration:easing:)`. The audio ramps down to silence over the last seconds of the block. ```swift highlight-fadeAudio-fade-out // Ramp down to silence over the last 2 seconds of the clip. try engine.block.setAudioFadeOut(audioBlock, duration: 2.0) ``` If the fade-out is longer than the clip, the window is clamped to the block, so the clip starts partly faded out. ## Choosing an Easing Curve Both methods take an optional `AnimationEasing` curve as their third argument, defaulting to `.linear`. `.linear` changes the gain at a constant rate, while `.easeIn`, `.easeOut`, and `.easeInOut` bias the ramp towards the start, the end, or both. The full set of curves is the same one used by block animations, including the quart, quint, back, and spring variants. ```swift highlight-fadeAudio-easing // A second clip that eases in and out instead of ramping linearly. let easedAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: easedAudio) try engine.block.setTimeOffset(easedAudio, offset: 14) try engine.block.setDuration(easedAudio, duration: 12) try engine.block.setAudioFadeIn(easedAudio, duration: 3.0, easing: .easeInOut) try engine.block.setAudioFadeOut(easedAudio, duration: 3.0, easing: .easeOut) ``` ## Fading Video Audio Video audio lives on the video fill, not on the graphic block. Resolve the fill with `getFill(_:)` and set the fade on it, exactly as with `setVolume(_:volume:)`. The fade window still follows the graphic block's position and duration on the timeline. ```swift highlight-fadeAudio-video-fill // Video audio lives on the video fill, so resolve the fill first — exactly as with `setVolume`. let fill = try engine.block.getFill(videoBlock) try engine.block.setAudioFadeIn(fill, duration: 1.5) try engine.block.setAudioFadeOut(fill, duration: 1.5, easing: .easeOut) ``` ## Reading the Fade Configuration Fades are exposed as block properties, so `getDouble(_:property:)` and `getEnum(_:property:)` read the current configuration back for UI controls. ```swift highlight-fadeAudio-read-fades // Read the configuration back through the block properties to drive UI controls. let fadeInDuration = try engine.block.getDouble(audioBlock, property: "playback/fadeIn/duration") let fadeInEasing = try engine.block.getEnum(audioBlock, property: "playback/fadeIn/easing") let fadeOutDuration = try engine.block.getDouble(audioBlock, property: "playback/fadeOut/duration") let fadeOutEasing = try engine.block.getEnum(audioBlock, property: "playback/fadeOut/easing") print("Fade in: \(fadeInDuration)s (\(fadeInEasing))") print("Fade out: \(fadeOutDuration)s (\(fadeOutEasing))") ``` | Property | Type | Description | | --------------------------- | ------ | ------------------------------------ | | `playback/fadeIn/duration` | Double | Fade-in duration in seconds | | `playback/fadeIn/easing` | Enum | Easing curve of the fade-in | | `playback/fadeOut/duration` | Double | Fade-out duration in seconds | | `playback/fadeOut/easing` | Enum | Easing curve of the fade-out | ## Removing a Fade Setting a duration of `0` removes a fade. ```swift highlight-fadeAudio-remove-fade // A duration of 0 removes a fade again. try engine.block.setAudioFadeOut(audioBlock, duration: 0) let removedFadeOut = try engine.block.getDouble(audioBlock, property: "playback/fadeOut/duration") print("Fade out after removal: \(removedFadeOut)s") ``` ## Fades, Trimming, and Splitting Because the windows are anchored to the block's timeline duration, shortening a clip moves the fade-out with the new end rather than leaving it stranded inside the clip. Splitting a clip keeps only the outer fades — the first half keeps its fade-in and the second half keeps its fade-out — so the audio does not dip at the cut. ## Troubleshooting ### Fade Is Not Audible Check that the block is not muted with `isMuted(_:)` and that its volume is above `0`. The fade gain multiplies the block's volume, so a fade on a silent block stays silent. ### Setting a Fade Fails Fades apply only to audio blocks and video fills. Pass the video fill returned by `getFill(_:)`, not the graphic block that owns it. ### Fade-Out Is Longer Than the Clip The window is clamped to the block, so the clip starts partly faded out. Shorten the fade or lengthen the clip. ### Fade Disappeared After Splitting Each half of a split keeps only the fade at its outer edge, so the fade-out of the first half and the fade-in of the second half are dropped. ## API Reference ### Methods | Method | Description | | --------------------------------------------- | -------------------------------------------------- | | `block.setAudioFadeIn(_:duration:easing:)` | Ramp audio up at the start of the block | | `block.setAudioFadeOut(_:duration:easing:)` | Ramp audio down at the end of the block | | `block.getDouble(_:property:)` | Read a fade duration | | `block.getEnum(_:property:)` | Read a fade easing curve | | `block.setVolume(_:volume:)` | Set the volume the fade ramps towards | | `block.getFill(_:)` | Resolve the video fill that carries the audio | | `block.forceLoadAVResource(_:)` | Load the audio resource and its metadata | ## Next Steps - [Adjust Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/) — Set the level a fade ramps towards - [Add Music](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-music-5b182c/) — Add background music and audio tracks - [Apply Transitions](https://img.ly/docs/cesdk/mac-catalyst/create-video/apply-transitions-146026/) — Cross-fade between clips on a track - [Split](https://img.ly/docs/cesdk/mac-catalyst/edit-video/split-464167/) — Split clips on the timeline - [Trim](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) — Trim clips without stranding their fades --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Loop Audio" description: "Control audio looping behavior programmatically using CE.SDK's headless engine for Swift-based audio processing and automated content workflows." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/loop-937be7/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio-2f700b/) > [Loop](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/loop-937be7/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-audio-loop/CreateAudioLoop.swift reference-only import Foundation import IMGLYEngine @MainActor func createAudioLoop(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setDuration(page, duration: 30) let baseURL = try engine.guidesBaseURL let audioURL = baseURL.appendingPathComponent("ly.img.audio/audios/far_from_home.m4a") // Create an audio block and set the audio source let audioBlock = try engine.block.create(.audio) try engine.block.setURL(audioBlock, property: "audio/fileURI", value: audioURL) // Load the audio resource to access metadata try await engine.block.forceLoadAVResource(audioBlock) // Get the total audio duration from the loaded resource let audioDuration = try engine.block.getDouble(audioBlock, property: "audio/totalDuration") print("Audio duration: \(String(format: "%.2f", audioDuration)) seconds") // Enable looping: a 5-second audio with a 15-second duration loops three times let loopingAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: loopingAudio) try engine.block.setTimeOffset(loopingAudio, offset: 0) try engine.block.setLooping(loopingAudio, looping: true) try engine.block.setDuration(loopingAudio, duration: 15) // Check whether looping is enabled on the block let isLooping = try engine.block.isLooping(loopingAudio) print("Is looping: \(isLooping)") // Disable looping: the audio plays once and leaves silence for the remaining duration let nonLoopingAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: nonLoopingAudio) try engine.block.setTimeOffset(nonLoopingAudio, offset: 16) try engine.block.setLooping(nonLoopingAudio, looping: false) try engine.block.setDuration(nonLoopingAudio, duration: 12) // Combine trim settings with looping to repeat a short segment // A 2-second segment (1.0s–3.0s) with an 8-second duration loops four times let trimmedLoopAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: trimmedLoopAudio) try engine.block.setTimeOffset(trimmedLoopAudio, offset: 29) try engine.block.setTrimOffset(trimmedLoopAudio, offset: 1.0) try engine.block.setTrimLength(trimmedLoopAudio, length: 2.0) try engine.block.setLooping(trimmedLoopAudio, looping: true) try engine.block.setDuration(trimmedLoopAudio, duration: 8.0) // Remove the original unparented audio block try engine.block.destroy(audioBlock) // Save the scene to a string for storage or later rendering let sceneString = try await engine.scene.saveToString() print("Scene saved (\(sceneString.count) characters)") _ = audioDuration _ = isLooping _ = nonLoopingAudio _ = trimmedLoopAudio _ = sceneString } ``` Control audio looping behavior programmatically using CE.SDK's headless Swift engine for audio processing and automated content workflows. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-audio-loop) Audio looping allows media to play continuously by restarting from the beginning when it reaches the end. When you set a block's duration longer than the audio length and enable looping, CE.SDK automatically repeats the audio to fill the entire duration. This guide covers how to enable and disable audio looping, control looping behavior with duration settings, and loop trimmed audio segments using the headless Swift engine. ## Setting Up the Scene We create a video scene with a page that acts as the timeline container. Setting a page duration reserves space for multiple audio blocks arranged over time. ```swift highlight-createAudioLoop-setup let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setDuration(page, duration: 30) ``` All audio blocks must be children of the page to appear on the timeline. ## Understanding Audio Looping When looping is enabled on an audio block, CE.SDK repeats the audio content from the beginning each time it reaches the end. This continues until the block's duration is filled. For example, a 5-second audio clip with looping enabled and a 15-second duration plays three complete times. The loop transitions are seamless — CE.SDK jumps immediately from the end back to the beginning without gaps or clicks. The audio content itself determines how smooth the loop sounds. Audio files designed for looping (with matching start and end points) create perfectly seamless loops. ## Creating Audio Blocks ### Adding Audio Content Audio blocks use file URIs to reference audio sources. We create the block and set the audio source using the `audio/fileURI` property. ```swift highlight-createAudioLoop-createAudioBlock // Create an audio block and set the audio source let audioBlock = try engine.block.create(.audio) try engine.block.setURL(audioBlock, property: "audio/fileURI", value: audioURL) ``` CE.SDK supports common audio formats including MP3, M4A, WAV, and AAC. ## Enabling Audio Looping ### Loading Audio Resources Before working with audio properties like duration or trim, we load the audio resource to ensure metadata is available. ```swift highlight-createAudioLoop-loadAudioResource // Load the audio resource to access metadata try await engine.block.forceLoadAVResource(audioBlock) // Get the total audio duration from the loaded resource let audioDuration = try engine.block.getDouble(audioBlock, property: "audio/totalDuration") print("Audio duration: \(String(format: "%.2f", audioDuration)) seconds") ``` Loading the resource provides access to the total audio duration via the `audio/totalDuration` property, which helps calculate how many times the audio will loop given a specific block duration. ### Setting Looping State We enable looping by calling `setLooping(_:looping:)` with `true`. When combined with a block duration longer than the audio length, the audio repeats to fill the full duration. ```swift highlight-createAudioLoop-enableLooping // Enable looping: a 5-second audio with a 15-second duration loops three times let loopingAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: loopingAudio) try engine.block.setTimeOffset(loopingAudio, offset: 0) try engine.block.setLooping(loopingAudio, looping: true) try engine.block.setDuration(loopingAudio, duration: 15) ``` In this example, if the audio is 5 seconds long and the block duration is 15 seconds, the audio loops three times (5 seconds × 3 = 15 seconds total). ## Querying and Controlling Looping ### Checking Looping State We can check whether an audio block has looping enabled at any time using `isLooping(_:)`. ```swift highlight-createAudioLoop-queryLoopingState // Check whether looping is enabled on the block let isLooping = try engine.block.isLooping(loopingAudio) print("Is looping: \(isLooping)") ``` This is useful when managing multiple audio tracks, allowing you to query and update looping states dynamically. ### Disabling Looping To play audio once without repeating, set looping to `false`. ```swift highlight-createAudioLoop-nonLoopingAudio // Disable looping: the audio plays once and leaves silence for the remaining duration let nonLoopingAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: nonLoopingAudio) try engine.block.setTimeOffset(nonLoopingAudio, offset: 16) try engine.block.setLooping(nonLoopingAudio, looping: false) try engine.block.setDuration(nonLoopingAudio, duration: 12) ``` With looping disabled and a duration longer than the audio length, the audio plays once and then stops, leaving silence for the remaining duration. ## Looping with Trim Settings ### Trimming Looped Audio We can combine trimming with looping to create short repeating segments from longer audio files. ```swift highlight-createAudioLoop-loopingWithTrim // Combine trim settings with looping to repeat a short segment // A 2-second segment (1.0s–3.0s) with an 8-second duration loops four times let trimmedLoopAudio = try engine.block.duplicate(audioBlock) try engine.block.appendChild(to: page, child: trimmedLoopAudio) try engine.block.setTimeOffset(trimmedLoopAudio, offset: 29) try engine.block.setTrimOffset(trimmedLoopAudio, offset: 1.0) try engine.block.setTrimLength(trimmedLoopAudio, length: 2.0) try engine.block.setLooping(trimmedLoopAudio, looping: true) try engine.block.setDuration(trimmedLoopAudio, duration: 8.0) ``` This trims the audio to a 2-second segment (from 1.0s to 3.0s of the source), then loops that segment four times to fill an 8-second duration. This technique is useful for creating rhythmic loops or extracting repeatable portions from longer audio files. ### Choosing Loop Points For seamless loops, choose trim points where the audio content flows naturally from end to beginning. Audio with consistent rhythm, tone, and volume at trim boundaries creates the smoothest loops. Abrupt changes in content or volume at loop boundaries create noticeable transitions. ## Exporting the Scene After configuring audio looping, we save the scene for later use or rendering. The scene file preserves all looping settings and can be loaded in any CE.SDK environment. ```swift highlight-createAudioLoop-export // Save the scene to a string for storage or later rendering let sceneString = try await engine.scene.saveToString() print("Scene saved (\(sceneString.count) characters)") ``` The exported scene string contains all audio blocks with their looping configurations, ready for rendering with CE.SDK Renderer or further editing. ## Troubleshooting **Audio not looping**: Verify looping is enabled with `isLooping(_:)` and that the block duration exceeds the audio length. Looping only takes effect when the duration allows multiple repetitions. **Audible gaps at loop points**: Choose trim points where the audio naturally transitions from end to beginning. Audio with matching start and end volume levels creates smoother loops. **Resource not loaded**: Always call `forceLoadAVResource(_:)` before accessing duration properties. Without loading the resource first, metadata like total duration won't be available. ## API Reference | Method | Description | Parameters | Returns | | --- | --- | --- | --- | | `engine.block.create(.audio)` | Create an audio block | `.audio` type | `DesignBlockID` | | `engine.block.setString(_:property:value:)` | Set audio source URI | block ID, property, value | `Void` | | `engine.block.setLooping(_:looping:)` | Enable or disable audio looping | block ID, enabled | `Void` | | `engine.block.isLooping(_:)` | Check if audio is set to loop | block ID | `Bool` | | `engine.block.setDuration(_:duration:)` | Set block playback duration | block ID, duration | `Void` | | `engine.block.getDuration(_:)` | Get block duration | block ID | `Double` | | `engine.block.setTrimOffset(_:offset:)` | Set trim start point | block ID, offset | `Void` | | `engine.block.setTrimLength(_:length:)` | Set trim length | block ID, length | `Void` | | `engine.block.forceLoadAVResource(_:)` | Load audio resource with metadata | block ID | `Void` | | `engine.block.getDouble(_:property:)` | Get audio property value | block ID, property | `Double` | | `engine.scene.saveToString()` | Export scene as string | none | `String` | ## Next Steps - [Add Music](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-music-5b182c/) — Add background music and audio tracks to video projects using CE.SDK's audio block system. - [Adjust Audio Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/) — Control audio playback levels and balance multiple audio sources. - [Adjust Audio Playback Speed](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-speed-908d57/) — Create slow-motion, time-stretched, and fast-forward audio effects. - [Add Sound Effects](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-sound-effects-9e984e/) — Generate sound effects programmatically from raw PCM data using audio buffers. - [Record Voiceover](#broken-link-07e8e1) — On iOS, let users capture voiceover clips directly in the editor UI. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create Compositions" description: "Combine and arrange multiple elements to create complex, multi-page, or layered design compositions." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/create-composition/overview-5b19c5/) - Combine and arrange multiple elements to create complex, multi-page, or layered design compositions. - [Multi-Page Layouts](https://img.ly/docs/cesdk/mac-catalyst/create-composition/multi-page-4d2b50/) - Create and manage multi-page designs in CE.SDK for documents like brochures, presentations, and catalogs with multiple pages in a single scene. - [Create a Collage](https://img.ly/docs/cesdk/mac-catalyst/create-composition/collage-f7d28d/) - Create collages in Swift by loading a layout page and transferring images and text between pages. - [Design a Layout](https://img.ly/docs/cesdk/mac-catalyst/create-composition/layout-b66311/) - Documentation for Design a Layout - [Add a Background](https://img.ly/docs/cesdk/mac-catalyst/create-composition/add-background-375a47/) - Add backgrounds to designs using fills for pages and shapes, and the background color property for text blocks. - [Positioning and Alignment](https://img.ly/docs/cesdk/mac-catalyst/create-composition/position-and-align-cc6b6a/) - Precisely position, align, and distribute objects using guides, snapping, and alignment tools. - [Group and Ungroup Objects](https://img.ly/docs/cesdk/mac-catalyst/create-composition/group-and-ungroup-62565a/) - Group multiple blocks to move, scale, and transform them as a single unit; ungroup to edit them individually. - [Layer Management](https://img.ly/docs/cesdk/mac-catalyst/create-composition/layer-management-18f07a/) - Organize design elements using a layer stack for precise control over stacking and visibility. - [Lock Design](https://img.ly/docs/cesdk/mac-catalyst/create-composition/lock-design-0a81de/) - Protect design elements from unwanted modifications using CE.SDK's scope-based permission system. Control which properties users can edit at both global and block levels. - [Blend Modes](https://img.ly/docs/cesdk/mac-catalyst/create-composition/blend-modes-ad3519/) - Apply blend modes to elements to control how colors and layers interact visually. - [Masks and Mattes](https://img.ly/docs/cesdk/mac-catalyst/create-composition/masks-and-mattes-aa9c93/) - Show only part of a block with shape masks or with a track matte from another block. - [Programmatic Creation](https://img.ly/docs/cesdk/mac-catalyst/create-composition/programmatic-a688bf/) - Build compositions entirely through code with the CE.SDK Engine for automation, batch processing, and headless rendering. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Add a Background" description: "Add backgrounds to designs using fills for pages and shapes, and the background color property for text blocks." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/add-background-375a47/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Add a Background](https://img.ly/docs/cesdk/mac-catalyst/create-composition/add-background-375a47/) --- ```swift file=@cesdk_swift_examples/engine-guides-add-background/AddBackground.swift reference-only import Foundation import IMGLYEngine @MainActor func addBackground(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) if try engine.block.supportsFill(page) { let gradientFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops(gradientFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.85, g: 0.75, b: 0.95, a: 1.0), stop: 0), GradientColorStop(color: .rgba(r: 0.7, g: 0.9, b: 0.95, a: 1.0), stop: 1), ]) try engine.block.setFill(page, fill: gradientFill) } // Create a text block to demonstrate background color let textBlock = try engine.block.create(.text) try engine.block.setString(textBlock, property: "text/text", value: "Backgrounds") try engine.block.setFloat(textBlock, property: "text/fontSize", value: 48) try engine.block.setWidth(textBlock, value: 280) try engine.block.setHeightMode(textBlock, mode: .auto) try engine.block.setPositionX(textBlock, value: 66) try engine.block.setPositionY(textBlock, value: 280) try engine.block.appendChild(to: page, child: textBlock) if try engine.block.supportsBackgroundColor(textBlock) { try engine.block.setBackgroundColorEnabled(textBlock, enabled: true) try engine.block.setColor( textBlock, property: "backgroundColor/color", color: .rgba(r: 1.0, g: 1.0, b: 1.0, a: 1.0), ) try engine.block.setFloat(textBlock, property: "backgroundColor/paddingLeft", value: 16) try engine.block.setFloat(textBlock, property: "backgroundColor/paddingRight", value: 16) try engine.block.setFloat(textBlock, property: "backgroundColor/paddingTop", value: 10) try engine.block.setFloat(textBlock, property: "backgroundColor/paddingBottom", value: 10) try engine.block.setFloat(textBlock, property: "backgroundColor/cornerRadius", value: 8) } // Create a graphic block to demonstrate image fill on a shape let baseURL = try engine.guidesBaseURL let imageBlock = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(imageBlock, shape: rectShape) try engine.block.setWidth(imageBlock, value: 340) try engine.block.setHeight(imageBlock, value: 400) try engine.block.setPositionX(imageBlock, value: 420) try engine.block.setPositionY(imageBlock, value: 100) try engine.block.appendChild(to: page, child: imageBlock) if try engine.block.supportsFill(imageBlock) { let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) } let pageSupportsFill = try engine.block.supportsFill(page) // true let textSupportsBackground = try engine.block.supportsBackgroundColor(textBlock) // true let imageSupportsFill = try engine.block.supportsFill(imageBlock) // true } ``` Add backgrounds to designs using fills for pages and shapes, and the background color property for text blocks. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-add-background) CE.SDK provides two distinct approaches for adding backgrounds to design elements. Understanding when to use each approach ensures your designs render correctly and efficiently. ## Setup Create a scene with a page where we'll apply backgrounds. ```swift highlight-addBackground-setup let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) ``` ## Fills Fills are visual content applied to pages and graphic blocks. Supported fill types include solid colors, linear gradients, radial gradients, and images. ### Check Fill Support Before applying a fill, verify the block supports it with `supportsFill(_:)`. Pages and graphic blocks typically support fills, while text blocks handle their content differently. Use `supportsBackgroundColor(_:)` for the dedicated background color property available on text blocks. ### Apply a Gradient Fill Create a fill with `createFill(_:)` specifying the type, configure its color stops, then apply it with `setFill(_:fill:)`. The example below creates a linear gradient with two color stops transitioning from pastel purple to light cyan. ```swift highlight-addBackground-pageFill if try engine.block.supportsFill(page) { let gradientFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops(gradientFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.85, g: 0.75, b: 0.95, a: 1.0), stop: 0), GradientColorStop(color: .rgba(r: 0.7, g: 0.9, b: 0.95, a: 1.0), stop: 1), ]) try engine.block.setFill(page, fill: gradientFill) } ``` ### Apply an Image Fill Image fills display images within the block's shape bounds. Create an image fill, set its URI, and apply it to a graphic block. ```swift highlight-addBackground-shapeFill if try engine.block.supportsFill(imageBlock) { let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) } ``` Image fills automatically scale to cover the shape area. ## Background Color Background color is a dedicated property available specifically on text blocks. Unlike fills, background colors include configurable padding and corner radius, creating highlighted text effects without additional graphic blocks. ### Apply Background Color Enable the background color with `setBackgroundColorEnabled(_:enabled:)`, then configure its appearance using property paths for color, padding, and corner radius. ```swift highlight-addBackground-backgroundColor if try engine.block.supportsBackgroundColor(textBlock) { try engine.block.setBackgroundColorEnabled(textBlock, enabled: true) try engine.block.setColor( textBlock, property: "backgroundColor/color", color: .rgba(r: 1.0, g: 1.0, b: 1.0, a: 1.0), ) try engine.block.setFloat(textBlock, property: "backgroundColor/paddingLeft", value: 16) try engine.block.setFloat(textBlock, property: "backgroundColor/paddingRight", value: 16) try engine.block.setFloat(textBlock, property: "backgroundColor/paddingTop", value: 10) try engine.block.setFloat(textBlock, property: "backgroundColor/paddingBottom", value: 10) try engine.block.setFloat(textBlock, property: "backgroundColor/cornerRadius", value: 8) } ``` The padding properties (`backgroundColor/paddingLeft`, `backgroundColor/paddingRight`, `backgroundColor/paddingTop`, `backgroundColor/paddingBottom`) control the space between the text and the background edge. The `backgroundColor/cornerRadius` property rounds the corners. ## Check Feature Support Use `supportsFill(_:)` to check whether a block supports fills, and `supportsBackgroundColor(_:)` to check whether a block supports the background color property. Always verify support before calling related APIs. ```swift highlight-addBackground-checkSupport let pageSupportsFill = try engine.block.supportsFill(page) // true let textSupportsBackground = try engine.block.supportsBackgroundColor(textBlock) // true let imageSupportsFill = try engine.block.supportsFill(imageBlock) // true ``` ## API Reference | Method | Description | | --- | --- | | `engine.block.supportsFill(_:)` | Check if a block supports fills | | `engine.block.createFill(_:)` | Create a fill (color, linearGradient, radialGradient, image) | | `engine.block.setFill(_:fill:)` | Apply a fill to a block | | `engine.block.getFill(_:)` | Get the fill applied to a block | | `engine.block.setGradientColorStops(_:property:colors:)` | Set gradient color stops | | `engine.block.supportsBackgroundColor(_:)` | Check if a block supports background color | | `engine.block.setBackgroundColorEnabled(_:enabled:)` | Enable or disable background color | | `engine.block.isBackgroundColorEnabled(_:)` | Check if background color is enabled | | `engine.block.setColor(_:property:color:)` | Set color properties | | `engine.block.setFloat(_:property:value:)` | Set float properties (padding, radius) | ## Next Steps - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) — Work with RGB, CMYK, and spot colors - [Fills Overview](https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/) — Learn about all fill types in depth --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Blend Modes" description: "Apply blend modes to elements to control how colors and layers interact visually." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/blend-modes-ad3519/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Blend Modes](https://img.ly/docs/cesdk/mac-catalyst/create-composition/blend-modes-ad3519/) --- ```swift file=@cesdk_swift_examples/engine-guides-blend-modes/BlendModes.swift reference-only import IMGLYEngine @MainActor func blendModes(engine: Engine) async throws { // Set up a scene with a page and two overlapping graphic blocks let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Create a background graphic block (base layer) let background = try engine.block.create(.graphic) try engine.block.setShape(background, shape: engine.block.createShape(.rect)) try engine.block.setWidth(background, value: 400) try engine.block.setHeight(background, value: 400) try engine.block.setPositionX(background, value: 200) try engine.block.setPositionY(background, value: 100) let backgroundFill = try engine.block.createFill(.color) try engine.block.setColor(backgroundFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.5, b: 0.0, a: 1.0)) try engine.block.setFill(background, fill: backgroundFill) try engine.block.appendChild(to: page, child: background) // Create a top graphic block to blend with the background let overlay = try engine.block.create(.graphic) try engine.block.setShape(overlay, shape: engine.block.createShape(.rect)) try engine.block.setWidth(overlay, value: 400) try engine.block.setHeight(overlay, value: 400) try engine.block.setPositionX(overlay, value: 200) try engine.block.setPositionY(overlay, value: 100) let overlayFill = try engine.block.createFill(.color) try engine.block.setColor(overlayFill, property: "fill/color/value", color: .rgba(r: 0.0, g: 0.5, b: 1.0, a: 1.0)) try engine.block.setFill(overlay, fill: overlayFill) try engine.block.appendChild(to: page, child: overlay) // Verify the block supports blend modes before applying one let supportsBlend = try engine.block.supportsBlendMode(overlay) print("Supports blend mode:", supportsBlend) // true // Apply the Multiply blend mode to the top block if supportsBlend { try engine.block.setBlendMode(overlay, mode: .multiply) } // Retrieve the current blend mode to confirm the change let currentMode = try engine.block.getBlendMode(overlay) print("Current blend mode:", currentMode) // BlendMode.multiply // Combine the blend mode with reduced opacity for a softer effect if try engine.block.supportsOpacity(overlay) { try engine.block.setOpacity(overlay, value: 0.7) } // Read back the current opacity value let currentOpacity = try engine.block.getOpacity(overlay) print("Current opacity:", currentOpacity) // 0.7 } ``` Control how design blocks visually blend with underlying layers using CE.SDK's blend mode system for professional layered compositions. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-blend-modes) Blend modes control how a block's colors combine with underlying layers, similar to blend modes in Photoshop or other design tools. CE.SDK provides 27 blend modes organized into categories: Normal, Darken, Lighten, Contrast, Inversion, and Component. Each category serves different compositing needs — darken modes make images darker, lighten modes make them brighter, and contrast modes increase midtone contrast. This guide covers how to check blend mode support, apply blend modes programmatically, understand the available blend mode options, and combine blend modes with opacity for fine control over layer compositing. ## Checking Blend Mode Support Before applying a blend mode, verify that the block supports it using `supportsBlendMode(_:)`. Most graphic blocks support blend modes, but checking avoids runtime errors. ```swift highlight-blendModes-checkSupport // Verify the block supports blend modes before applying one let supportsBlend = try engine.block.supportsBlendMode(overlay) print("Supports blend mode:", supportsBlend) // true ``` Blend mode support is available for graphic blocks with image or color fills, shape blocks, and text blocks. Page and scene blocks do not support blend modes directly. ## Setting and Getting Blend Modes Apply a blend mode with `setBlendMode(_:mode:)` and retrieve the current mode with `getBlendMode(_:)`. The default blend mode is `.normal`, which displays the block without any blending effect. ```swift highlight-blendModes-setBlendMode // Apply the Multiply blend mode to the top block if supportsBlend { try engine.block.setBlendMode(overlay, mode: .multiply) } ``` After setting a blend mode, confirm the change by reading it back: ```swift highlight-blendModes-getBlendMode // Retrieve the current blend mode to confirm the change let currentMode = try engine.block.getBlendMode(overlay) print("Current blend mode:", currentMode) // BlendMode.multiply ``` ## Available Blend Modes CE.SDK provides 27 blend modes organized into categories, each producing different visual results: ### Normal Modes - **`.passThrough`** — Allows children of a group to blend with layers below the group - **`.normal`** — Default mode with no blending effect ### Darken Modes These modes darken the result by comparing the base and blend colors: - **`.darken`** — Selects the darker of the base and blend colors - **`.multiply`** — Multiplies colors, producing darker results (great for shadows) - **`.colorBurn`** — Darkens base color by increasing contrast - **`.linearBurn`** — Darkens base color by decreasing brightness - **`.darkenColor`** — Selects the darker color based on luminosity ### Lighten Modes These modes lighten the result by comparing colors: - **`.lighten`** — Selects the lighter of the base and blend colors - **`.screen`** — Multiplies the inverse of colors, producing lighter results (great for highlights) - **`.colorDodge`** — Lightens base color by decreasing contrast - **`.linearDodge`** — Lightens base color by increasing brightness - **`.lightenColor`** — Selects the lighter color based on luminosity ### Contrast Modes These modes increase midtone contrast: - **`.overlay`** — Combines Multiply and Screen based on the base color - **`.softLight`** — Similar to Overlay but with a softer effect - **`.hardLight`** — Similar to Overlay but based on the blend color - **`.vividLight`** — Burns or dodges colors based on the blend color - **`.linearLight`** — Increases or decreases brightness based on blend color - **`.pinLight`** — Replaces colors based on the blend color - **`.hardMix`** — Reduces colors to white, black, or primary colors ### Inversion Modes These modes create inverted or subtracted effects: - **`.difference`** — Subtracts the darker from the lighter color - **`.exclusion`** — Similar to Difference with lower contrast - **`.subtract`** — Subtracts blend color from base color - **`.divide`** — Divides base color by blend color ### Component Modes These modes affect specific color components: - **`.hue`** — Uses the hue of the blend color with base saturation and luminosity - **`.saturation`** — Uses the saturation of the blend color - **`.color`** — Uses the hue and saturation of the blend color - **`.luminosity`** — Uses the luminosity of the blend color ## Combining Blend Modes with Opacity For finer control over compositing, combine blend modes with opacity. Opacity reduces overall visibility while the blend mode affects color interaction with underlying layers. ```swift highlight-blendModes-setOpacity // Combine the blend mode with reduced opacity for a softer effect if try engine.block.supportsOpacity(overlay) { try engine.block.setOpacity(overlay, value: 0.7) } ``` Read back the current opacity value to confirm changes or inspect existing state: ```swift highlight-blendModes-getOpacity // Read back the current opacity value let currentOpacity = try engine.block.getOpacity(overlay) print("Current opacity:", currentOpacity) // 0.7 ``` > **Tip:** Start with full opacity (1.0) when experimenting with blend modes, then reduce > opacity to soften the effect. Common values are 0.5–0.7 for subtle blending > effects. ## API Reference | Method | Description | | --- | --- | | `engine.block.supportsBlendMode(_:)` | Check if a block supports blend modes | | `engine.block.setBlendMode(_:mode:)` | Set the blend mode for a block | | `engine.block.getBlendMode(_:)` | Get the current blend mode of a block | | `engine.block.supportsOpacity(_:)` | Check if a block supports opacity | | `engine.block.setOpacity(_:value:)` | Set the opacity for a block (0–1) | | `engine.block.getOpacity(_:)` | Get the current opacity of a block | ## Next Steps - [Layer Management](https://img.ly/docs/cesdk/mac-catalyst/create-composition/layer-management-18f07a/) — Control z-order and visibility of blocks - [Add a Background](https://img.ly/docs/cesdk/mac-catalyst/create-composition/add-background-375a47/) — Set page backgrounds that blend modes composite against - [Grouping](https://img.ly/docs/cesdk/mac-catalyst/create-composition/group-and-ungroup-62565a/) — Combine blocks to apply blend modes to groups --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create a Collage" description: "Create collages in Swift by loading a layout page and transferring images and text between pages." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/collage-f7d28d/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Create a Collage](https://img.ly/docs/cesdk/mac-catalyst/create-composition/collage-f7d28d/) --- ```swift file=@cesdk_swift_examples/engine-guides-collage/Collage.swift reference-only import Foundation import IMGLYEngine @MainActor func collage(engine: Engine) async throws { // Demo scaffolding: build a source scene with two images and one caption so the // collage workflow has content to transfer. In a real app this is whatever the // user already has open in the editor. let scene = try engine.scene.create() let baseURL = try engine.guidesBaseURL let sourcePage = try makeCollagePage(engine: engine, width: 1080, height: 1080) try engine.block.appendChild(to: scene, child: sourcePage) let leftImage = try makeImageSlot( engine: engine, x: 40, y: 40, width: 480, height: 1000, uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.appendChild(to: sourcePage, child: leftImage) let rightImage = try makeImageSlot( engine: engine, x: 560, y: 40, width: 480, height: 760, uri: baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg"), ) try engine.block.appendChild(to: sourcePage, child: rightImage) let caption = try makeTextBlock( engine: engine, x: 560, y: 820, width: 480, text: "Summer Memories", color: .rgba(r: 0.05, g: 0.05, b: 0.05, a: 1), ) try engine.block.appendChild(to: sourcePage, child: caption) try await engine.captureGuide(sourcePage, label: "before-layout") let layoutData = try await makeFourUpLayout(engine: engine, baseURL: baseURL, width: 1080, height: 1080) let collagedPage = try await applyCollageLayout( engine: engine, page: sourcePage, layoutData: layoutData, addUndoStep: true, ) try await engine.captureGuide(collagedPage, label: "hero") } // MARK: - Page and Slot Helpers @MainActor private func makeCollagePage( engine: Engine, width: Float, height: Float, ) throws -> DesignBlockID { let page = try engine.block.create(.page) try engine.block.setWidth(page, value: width) try engine.block.setHeight(page, value: height) return page } @MainActor private func makeImageSlot( engine: Engine, x: Float, y: Float, width: Float, height: Float, uri: URL, ) throws -> DesignBlockID { let graphic = try engine.block.create(.graphic) try engine.block.setShape(graphic, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: uri) try engine.block.setFill(graphic, fill: fill) try engine.block.setPositionX(graphic, value: x) try engine.block.setPositionY(graphic, value: y) try engine.block.setWidth(graphic, value: width) try engine.block.setHeight(graphic, value: height) return graphic } @MainActor private func makeTextBlock( engine: Engine, x: Float, y: Float, width: Float, text: String, color: Color, ) throws -> DesignBlockID { let block = try engine.block.create(.text) try engine.block.replaceText(block, text: text) try engine.block.setColor(block, property: "fill/solid/color", color: color) try engine.block.setPositionX(block, value: x) try engine.block.setPositionY(block, value: y) try engine.block.setWidth(block, value: width) return block } // MARK: - Define a Layout @MainActor private func makeFourUpLayout( engine: Engine, baseURL: URL, width: Float, height: Float, ) async throws -> String { let layoutPage = try makeCollagePage(engine: engine, width: width, height: height) let halfWidth = (width - 60) / 2 let halfHeight = (height - 60) / 2 - 40 let placeholders = [ baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_3.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_5.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_6.jpg"), ] try engine.block.appendChild(to: layoutPage, child: try makeImageSlot( engine: engine, x: 20, y: 20, width: halfWidth, height: halfHeight, uri: placeholders[0], )) try engine.block.appendChild(to: layoutPage, child: try makeImageSlot( engine: engine, x: 40 + halfWidth, y: 20, width: halfWidth, height: halfHeight, uri: placeholders[1], )) try engine.block.appendChild(to: layoutPage, child: try makeImageSlot( engine: engine, x: 20, y: 40 + halfHeight, width: halfWidth, height: halfHeight, uri: placeholders[2], )) try engine.block.appendChild(to: layoutPage, child: try makeImageSlot( engine: engine, x: 40 + halfWidth, y: 40 + halfHeight, width: halfWidth, height: halfHeight, uri: placeholders[3], )) try engine.block.appendChild(to: layoutPage, child: try makeTextBlock( engine: engine, x: 20, y: height - 60, width: width - 40, text: "Layout Caption", color: .rgba(r: 0.2, g: 0.2, b: 0.2, a: 1), )) let saved = try await engine.block.saveToString(blocks: [layoutPage]) try engine.block.destroy(layoutPage) return saved } // MARK: - Apply a Layout to a Page @MainActor private func applyCollageLayout( engine: Engine, page: DesignBlockID, layoutData: String, addUndoStep: Bool, ) async throws -> DesignBlockID { let previousDestroyScope = try engine.editor.getGlobalScope(key: "lifecycle/destroy") try engine.editor.setGlobalScope(key: "lifecycle/destroy", value: .allow) defer { try? engine.editor.setGlobalScope(key: "lifecycle/destroy", value: previousDestroyScope) } for selected in engine.block.findAllSelected() { try engine.block.setSelected(selected, selected: false) } let oldPageBackup = try engine.block.duplicate(page, attachToParent: false) let loadedBlocks = try await engine.block.load(from: layoutData) guard let layoutPage = loadedBlocks.first else { throw NSError(domain: "Collage", code: 1, userInfo: [ NSLocalizedDescriptionKey: "Saved layout string did not contain a page.", ]) } for child in try engine.block.getChildren(page) { try engine.block.destroy(child) } for (index, child) in try engine.block.getChildren(layoutPage).enumerated() { try engine.block.insertChild(into: page, child: child, at: index) } try transferCollageContent(engine: engine, from: oldPageBackup, to: page) try engine.block.destroy(oldPageBackup) try engine.block.destroy(layoutPage) if addUndoStep { try engine.editor.addUndoStep() } return page } // MARK: - Transfer Content Between Pages @MainActor private func transferCollageContent( engine: Engine, from sourcePage: DesignBlockID, to targetPage: DesignBlockID, ) throws { let sourceBlocks = try visuallySortBlocks( engine: engine, blocks: collectDescendants(engine: engine, root: sourcePage), ) let targetBlocks = try visuallySortBlocks( engine: engine, blocks: collectDescendants(engine: engine, root: targetPage), ) let sourceImages = try sourceBlocks.filter { try isImageSlot(engine: engine, block: $0) } let targetImages = try targetBlocks.filter { try isImageSlot(engine: engine, block: $0) } for (source, target) in zip(sourceImages, targetImages) { try copyImage(engine: engine, from: source, to: target) } let sourceTexts = try sourceBlocks.filter { try engine.block.getType($0) == DesignBlockType.text.rawValue } let targetTexts = try targetBlocks.filter { try engine.block.getType($0) == DesignBlockType.text.rawValue } for (source, target) in zip(sourceTexts, targetTexts) { try copyText(engine: engine, from: source, to: target) } } // MARK: - Visual Sort private struct SortedBlock { let block: DesignBlockID let x: Int let y: Int } @MainActor private func collectDescendants( engine: Engine, root: DesignBlockID, ) throws -> [DesignBlockID] { var result: [DesignBlockID] = [] for child in try engine.block.getChildren(root) { result.append(child) result.append(contentsOf: try collectDescendants(engine: engine, root: child)) } return result } @MainActor private func accumulatedPosition( engine: Engine, block: DesignBlockID, ) throws -> (x: Int, y: Int) { var x: Float = 0 var y: Float = 0 var current: DesignBlockID? = block while let id = current { x += try engine.block.getPositionX(id) y += try engine.block.getPositionY(id) current = try engine.block.getParent(id) } return (Int(x.rounded()), Int(y.rounded())) } @MainActor private func visuallySortBlocks( engine: Engine, blocks: [DesignBlockID], ) throws -> [DesignBlockID] { let measured = try blocks.map { block -> SortedBlock in let position = try accumulatedPosition(engine: engine, block: block) return SortedBlock(block: block, x: position.x, y: position.y) } return measured .sorted { lhs, rhs in if lhs.y == rhs.y { return lhs.x < rhs.x } return lhs.y < rhs.y } .map(\.block) } // MARK: - Copy Images @MainActor private func isImageSlot( engine: Engine, block: DesignBlockID, ) throws -> Bool { guard try engine.block.getType(block) == DesignBlockType.graphic.rawValue else { return false } guard try engine.block.supportsFill(block) else { return false } let fill = try engine.block.getFill(block) return try engine.block.getType(fill) == FillType.image.rawValue } @MainActor private func copyImage( engine: Engine, from source: DesignBlockID, to target: DesignBlockID, ) throws { let sourceFill = try engine.block.getFill(source) let targetFill = try engine.block.getFill(target) let uri = try engine.block.getString(sourceFill, property: "fill/image/imageFileURI") try engine.block.setString(targetFill, property: "fill/image/imageFileURI", value: uri) let sources = try engine.block.getSourceSet(sourceFill, property: "fill/image/sourceSet") try engine.block.setSourceSet(targetFill, property: "fill/image/sourceSet", sourceSet: sources) try engine.block.resetCrop(target) if try engine.block.supportsPlaceholderBehavior(source), try engine.block.supportsPlaceholderBehavior(target) { let enabled = try engine.block.isPlaceholderBehaviorEnabled(source) try engine.block.setPlaceholderBehaviorEnabled(target, enabled: enabled) } } // MARK: - Copy Text @MainActor private func copyText( engine: Engine, from source: DesignBlockID, to target: DesignBlockID, ) throws { let text = try engine.block.getString(source, property: "text/text") try engine.block.replaceText(target, text: text) // Font transfer is best-effort: an unresolved URI must not abort the // collage update. if let typeface = try? engine.block.getTypeface(source) { let fontURIString = try engine.block.getString(source, property: "text/fontFileUri") if let fontURL = URL(string: fontURIString) { try? engine.block.setFont(target, fontFileURL: fontURL, typeface: typeface) } } if let color: Color = try? engine.block.getColor(source, property: "fill/solid/color") { try engine.block.setColor(target, property: "fill/solid/color", color: color) } } ``` Create a collage in Swift by loading a layout page and transferring existing images and text into the new structure. ![A 4-up collage where the user's two photos fill the top row, the bottom row keeps the layout's distinct placeholder images because no source image is available for those slots, and the user's "Summer Memories" caption replaces the layout's caption text.](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-collage) Layouts are predefined page structures that arrange images and text in a composition. Unlike templates, which usually replace the whole scene, this workflow keeps the user's content and maps it into a new layout. The example uses the Engine directly. You can call the same layout application function from SwiftUI, an asset source callback, or any other workflow that lets a user choose a layout. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. ## What You'll Learn In this guide, you will learn how to: - Define a layout page that contains image slots and text placeholders. - Load the layout from a saved block string. - Replace the current page structure while preserving existing image and text content. - Sort blocks visually so content maps top-to-bottom and left-to-right. ## When to Use Layouts Use layout-based collages when the app needs to keep the current content but change its arrangement. Common examples include: - Photo collages - Grid layouts - Magazine spreads - Social media posts ## Difference Between Layouts and Templates Layouts can be represented as [custom assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/), but the apply behavior is different from a full template load: - **Templates** load a complete design and replace the current scene. - **Layouts** provide a new page structure while the app transfers the current content into matching slots. Preserving assets while changing the layout is app logic. CE.SDK provides the block, fill, text, and scene APIs needed to implement that logic. ## How Collages Work When a user chooses a collage layout, the app performs this sequence: 1. Load a layout page from a saved block string. 2. Duplicate the current page as a temporary content source. 3. Replace the current page's children with the layout's children. 4. Copy images and text from the old page into the new layout in visual order. 5. Clean up temporary blocks and add an undo step. Visual order matters. The example sorts simple, untransformed layout slots by their accumulated page coordinates so content maps predictably between the old page and the new layout. ## Create Page Helpers The example uses small helpers to create pages, image slots, and text blocks. The page helper sets only the page dimensions. ```swift highlight-collage-pageHelper @MainActor private func makeCollagePage( engine: Engine, width: Float, height: Float, ) throws -> DesignBlockID { let page = try engine.block.create(.page) try engine.block.setWidth(page, value: width) try engine.block.setHeight(page, value: height) return page } ``` Image slots are graphic blocks with rectangular shapes and image fills. In a real app the URIs come from a bundle, local storage, or remote media the user picked. ```swift highlight-collage-imageSlotHelper @MainActor private func makeImageSlot( engine: Engine, x: Float, y: Float, width: Float, height: Float, uri: URL, ) throws -> DesignBlockID { let graphic = try engine.block.create(.graphic) try engine.block.setShape(graphic, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: uri) try engine.block.setFill(graphic, fill: fill) try engine.block.setPositionX(graphic, value: x) try engine.block.setPositionY(graphic, value: y) try engine.block.setWidth(graphic, value: width) try engine.block.setHeight(graphic, value: height) return graphic } ``` Text blocks isolate the text creation and initial color setup. ```swift highlight-collage-textBlockHelper @MainActor private func makeTextBlock( engine: Engine, x: Float, y: Float, width: Float, text: String, color: Color, ) throws -> DesignBlockID { let block = try engine.block.create(.text) try engine.block.replaceText(block, text: text) try engine.block.setColor(block, property: "fill/solid/color", color: color) try engine.block.setPositionX(block, value: x) try engine.block.setPositionY(block, value: y) try engine.block.setWidth(block, value: width) return block } ``` ## Define a Layout A layout is a page with positioned image slots and optional text blocks. In production, save these pages as scene or block files and load them from the app bundle, a backend, or an asset source. ```swift highlight-collage-defineLayout @MainActor private func makeFourUpLayout( engine: Engine, baseURL: URL, width: Float, height: Float, ) async throws -> String { let layoutPage = try makeCollagePage(engine: engine, width: width, height: height) let halfWidth = (width - 60) / 2 let halfHeight = (height - 60) / 2 - 40 let placeholders = [ baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_3.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_5.jpg"), baseURL.appendingPathComponent("ly.img.image/images/sample_6.jpg"), ] try engine.block.appendChild(to: layoutPage, child: try makeImageSlot( engine: engine, x: 20, y: 20, width: halfWidth, height: halfHeight, uri: placeholders[0], )) try engine.block.appendChild(to: layoutPage, child: try makeImageSlot( engine: engine, x: 40 + halfWidth, y: 20, width: halfWidth, height: halfHeight, uri: placeholders[1], )) try engine.block.appendChild(to: layoutPage, child: try makeImageSlot( engine: engine, x: 20, y: 40 + halfHeight, width: halfWidth, height: halfHeight, uri: placeholders[2], )) try engine.block.appendChild(to: layoutPage, child: try makeImageSlot( engine: engine, x: 40 + halfWidth, y: 40 + halfHeight, width: halfWidth, height: halfHeight, uri: placeholders[3], )) try engine.block.appendChild(to: layoutPage, child: try makeTextBlock( engine: engine, x: 20, y: height - 60, width: width - 40, text: "Layout Caption", color: .rgba(r: 0.2, g: 0.2, b: 0.2, a: 1), )) let saved = try await engine.block.saveToString(blocks: [layoutPage]) try engine.block.destroy(layoutPage) return saved } ``` The example saves the layout page with `engine.block.saveToString(blocks:)` and later restores it with `engine.block.load(from:)`. The same pattern works when the string comes from a remote layout file. The freshly loaded layout blocks are not attached to a scene by default, so the caller decides where they go. ## Apply the Collage Call the app-owned layout helper with the current page, the Engine instance, and the saved layout data. Pass `addUndoStep: true` when the action should become a single undoable editor operation. ```swift highlight-collage-applyLayout let collagedPage = try await applyCollageLayout( engine: engine, page: sourcePage, layoutData: layoutData, addUndoStep: true, ) ``` The function returns the page that now contains the collage structure and transferred content. ## Replace the Page Structure `applyCollageLayout(engine:page:layoutData:addUndoStep:)` temporarily allows block deletion, clears the current selection, duplicates the old page as a backup, loads the saved layout, and moves the layout's children into the current page. It then hands the backup and the rebuilt page to `transferCollageContent(engine:from:to:)` (covered in the next section) before destroying the temporary blocks and committing the undo step. ```swift highlight-collage-layoutWorkflow @MainActor private func applyCollageLayout( engine: Engine, page: DesignBlockID, layoutData: String, addUndoStep: Bool, ) async throws -> DesignBlockID { let previousDestroyScope = try engine.editor.getGlobalScope(key: "lifecycle/destroy") try engine.editor.setGlobalScope(key: "lifecycle/destroy", value: .allow) defer { try? engine.editor.setGlobalScope(key: "lifecycle/destroy", value: previousDestroyScope) } for selected in engine.block.findAllSelected() { try engine.block.setSelected(selected, selected: false) } let oldPageBackup = try engine.block.duplicate(page, attachToParent: false) let loadedBlocks = try await engine.block.load(from: layoutData) guard let layoutPage = loadedBlocks.first else { throw NSError(domain: "Collage", code: 1, userInfo: [ NSLocalizedDescriptionKey: "Saved layout string did not contain a page.", ]) } for child in try engine.block.getChildren(page) { try engine.block.destroy(child) } for (index, child) in try engine.block.getChildren(layoutPage).enumerated() { try engine.block.insertChild(into: page, child: child, at: index) } try transferCollageContent(engine: engine, from: oldPageBackup, to: page) try engine.block.destroy(oldPageBackup) try engine.block.destroy(layoutPage) if addUndoStep { try engine.editor.addUndoStep() } return page } ``` Key details: - Store and restore the previous `lifecycle/destroy` scope in a `defer` block so the surrounding editor state remains unchanged even when an error is thrown. - Keep the duplicate backup unattached so it cannot leak into the scene if transfer fails. - The old children get destroyed once the layout swap commits; the temporary backup and layout pages are destroyed after content transfer. ## Transfer Content `transferCollageContent(engine:from:to:)` collects all descendants from both pages, sorts each list by visual position, then pairs source and target blocks by type using `zip`. ```swift highlight-collage-transferContent @MainActor private func transferCollageContent( engine: Engine, from sourcePage: DesignBlockID, to targetPage: DesignBlockID, ) throws { let sourceBlocks = try visuallySortBlocks( engine: engine, blocks: collectDescendants(engine: engine, root: sourcePage), ) let targetBlocks = try visuallySortBlocks( engine: engine, blocks: collectDescendants(engine: engine, root: targetPage), ) let sourceImages = try sourceBlocks.filter { try isImageSlot(engine: engine, block: $0) } let targetImages = try targetBlocks.filter { try isImageSlot(engine: engine, block: $0) } for (source, target) in zip(sourceImages, targetImages) { try copyImage(engine: engine, from: source, to: target) } let sourceTexts = try sourceBlocks.filter { try engine.block.getType($0) == DesignBlockType.text.rawValue } let targetTexts = try targetBlocks.filter { try engine.block.getType($0) == DesignBlockType.text.rawValue } for (source, target) in zip(sourceTexts, targetTexts) { try copyText(engine: engine, from: source, to: target) } } ``` If the source has more images than the layout has slots, `zip` stops at the shorter list and the extra images are ignored. If the layout has more slots, the remaining slots keep their placeholder content. ## Sort Blocks Visually Block positions are local to their parent. For unrotated and unscaled layout slots, the example walks each block's ancestor chain with `getParent(_:)`, accumulates the offsets, rounds the values, then sorts by Y before X. If layout slots live under rotated or scaled parents, use the global bounding-box APIs instead of this local-offset helper. ```swift highlight-collage-visualSort private struct SortedBlock { let block: DesignBlockID let x: Int let y: Int } @MainActor private func collectDescendants( engine: Engine, root: DesignBlockID, ) throws -> [DesignBlockID] { var result: [DesignBlockID] = [] for child in try engine.block.getChildren(root) { result.append(child) result.append(contentsOf: try collectDescendants(engine: engine, root: child)) } return result } @MainActor private func accumulatedPosition( engine: Engine, block: DesignBlockID, ) throws -> (x: Int, y: Int) { var x: Float = 0 var y: Float = 0 var current: DesignBlockID? = block while let id = current { x += try engine.block.getPositionX(id) y += try engine.block.getPositionY(id) current = try engine.block.getParent(id) } return (Int(x.rounded()), Int(y.rounded())) } @MainActor private func visuallySortBlocks( engine: Engine, blocks: [DesignBlockID], ) throws -> [DesignBlockID] { let measured = try blocks.map { block -> SortedBlock in let position = try accumulatedPosition(engine: engine, block: block) return SortedBlock(block: block, x: position.x, y: position.y) } return measured .sorted { lhs, rhs in if lhs.y == rhs.y { return lhs.x < rhs.x } return lhs.y < rhs.y } .map(\.block) } ``` Keep layout slots in distinct positions when possible. Blocks with the same accumulated Y position map left-to-right. ## Identify Image Slots The transfer code treats a graphic block with an image fill as an image slot. This keeps the mapping independent from custom metadata or asset-source IDs. ```swift highlight-collage-imageSlotCheck @MainActor private func isImageSlot( engine: Engine, block: DesignBlockID, ) throws -> Bool { guard try engine.block.getType(block) == DesignBlockType.graphic.rawValue else { return false } guard try engine.block.supportsFill(block) else { return false } let fill = try engine.block.getFill(block) return try engine.block.getType(fill) == FillType.image.rawValue } ``` ## Copy Images Copy the image URI from the source fill to the target fill, copy the source set so responsive variants survive, and reset the crop so the image refits the new slot dimensions. The placeholder behavior calls preserve placeholder state when both blocks support it. ```swift highlight-collage-copyImages @MainActor private func copyImage( engine: Engine, from source: DesignBlockID, to target: DesignBlockID, ) throws { let sourceFill = try engine.block.getFill(source) let targetFill = try engine.block.getFill(target) let uri = try engine.block.getString(sourceFill, property: "fill/image/imageFileURI") try engine.block.setString(targetFill, property: "fill/image/imageFileURI", value: uri) let sources = try engine.block.getSourceSet(sourceFill, property: "fill/image/sourceSet") try engine.block.setSourceSet(targetFill, property: "fill/image/sourceSet", sourceSet: sources) try engine.block.resetCrop(target) if try engine.block.supportsPlaceholderBehavior(source), try engine.block.supportsPlaceholderBehavior(target) { let enabled = try engine.block.isPlaceholderBehaviorEnabled(source) try engine.block.setPlaceholderBehaviorEnabled(target, enabled: enabled) } } ``` ## Copy Text Text transfer reads the source string with `getString(_:property:)`, writes it with `replaceText(_:text:)`, and copies the block's fill color through the typed `Color` API. Typeface and font URI preservation is best-effort so unresolved fonts do not block the text content transfer. ```swift highlight-collage-copyText @MainActor private func copyText( engine: Engine, from source: DesignBlockID, to target: DesignBlockID, ) throws { let text = try engine.block.getString(source, property: "text/text") try engine.block.replaceText(target, text: text) // Font transfer is best-effort: an unresolved URI must not abort the // collage update. if let typeface = try? engine.block.getTypeface(source) { let fontURIString = try engine.block.getString(source, property: "text/fontFileUri") if let fontURL = URL(string: fontURIString) { try? engine.block.setFont(target, fontFileURL: fontURL, typeface: typeface) } } if let color: Color = try? engine.block.getColor(source, property: "fill/solid/color") { try engine.block.setColor(target, property: "fill/solid/color", color: color) } } ``` Use the same visual pairing rule for text as for images so captions and titles stay in their expected order. ## Connect It to Your UI The Engine workflow is UI-agnostic. A typical integration stores each layout with: | Field | Purpose | | --- | --- | | `id` | Stable layout identifier for the app | | `label` | Display name in the layout picker | | `uri` | Scene or block file containing the layout page | | `thumbnailUri` | Preview image shown in the UI | When a user selects a layout, load its file, pass the saved block string into the app's `applyCollageLayout(engine:page:layoutData:addUndoStep:)` helper, and keep the UI code separate from the content transfer logic. ## Troubleshooting | Issue | What to Check | | --- | --- | | Layout does not apply | Verify the saved layout string loads with `engine.block.load(from:)` and returns at least one block before moving children into the current page. | | Content maps to the wrong slots | Keep image and text slots unrotated and unscaled, and check the accumulated coordinates used by visual sorting. Blocks with the same accumulated Y coordinate map left-to-right, so keep slot positions distinct enough for predictable ordering. | | Images stay empty or lose variants | Ensure both source and target blocks use image fills, then copy `fill/image/imageFileURI` and `fill/image/sourceSet` before calling `engine.block.resetCrop(_:)`. | | Text copies without its expected font | The example wraps `engine.block.setFont(_:fontFileURL:typeface:)` in `try?`, so unresolved font URIs are skipped rather than aborting the collage. If you need stricter behavior, replace the `try?` with `try` and handle the error explicitly. | | Undo or cleanup behaves unexpectedly | Restore the previous `lifecycle/destroy` scope in a `defer` block, destroy the temporary duplicate and layout pages, and add the undo step only after transfer completes. | | Slot counts do not match | Pair source and target blocks with `zip`. Extra source content is ignored, and extra layout slots keep their placeholder content. | ## API Reference | API | Category | Purpose | | --- | --- | --- | | `engine.block.saveToString(blocks:)` | Layout data | Serialize a layout page so it can be stored as a layout asset or file. | | `engine.block.load(from:)` | Layout data | Load a saved layout page before moving its children into the current page. The loaded blocks are not attached to a scene by default. | | `engine.block.duplicate(_:attachToParent:)` | Backup | Copy the current page without attaching the duplicate to the scene. | | `engine.block.getChildren(_:)` | Hierarchy | Read child blocks before clearing or moving a page structure. | | `engine.block.insertChild(into:child:at:)` | Hierarchy | Move layout children into the current page in order. | | `engine.block.destroy(_:)` | Lifecycle | Remove old children and temporary pages during cleanup. | | `engine.editor.getGlobalScope(key:)` | Lifecycle | Store the current deletion scope before the layout swap. | | `engine.editor.setGlobalScope(key:value:)` | Lifecycle | Temporarily allow block deletion, then restore the previous scope. | | `engine.block.findAllSelected()` | Selection | Find selected blocks so the layout change can clear the selection first. | | `engine.block.setSelected(_:selected:)` | Selection | Deselect blocks before replacing the page structure. | | `engine.block.getFill(_:)` | Images | Access the image fill that stores URI and source-set properties. | | `engine.block.getString(_:property:)` | Images, Text | Read `fill/image/imageFileURI` from an image fill or `text/text` / `text/fontFileUri` from a text block. | | `engine.block.setURL(_:property:value:)` | Images | Set `fill/image/imageFileURI` on an image fill from a `URL`. | | `engine.block.setString(_:property:value:)` | Images | Copy `fill/image/imageFileURI` to the target fill. | | `engine.block.getSourceSet(_:property:)` | Images | Read responsive image variants from the source fill. | | `engine.block.setSourceSet(_:property:sourceSet:)` | Images | Preserve responsive image variants on the target fill. | | `engine.block.resetCrop(_:)` | Images | Refit the transferred image inside the new slot. | | `engine.block.supportsPlaceholderBehavior(_:)` | Placeholders | Check whether placeholder state can be copied. | | `engine.block.isPlaceholderBehaviorEnabled(_:)` | Placeholders | Read placeholder state from the source block. | | `engine.block.setPlaceholderBehaviorEnabled(_:enabled:)` | Placeholders | Apply the source placeholder behavior to the target image block. | | `engine.block.replaceText(_:text:)` | Text | Copy text content into the target block. | | `engine.block.getTypeface(_:)` | Text | Read the source typeface before applying the font to the target. | | `engine.block.setFont(_:fontFileURL:typeface:)` | Text | Preserve the source font when the URI and typeface resolve. | | `engine.block.getColor(_:property:)` / `setColor(_:property:color:)` | Text | Copy `fill/solid/color` through the typed `Color` API. | | `engine.block.getParent(_:)` | Sorting | Walk ancestors while accumulating untransformed page coordinates. | | `engine.block.getPositionX(_:)` / `getPositionY(_:)` | Sorting | Read local X / Y positions while calculating row-and-column ordering. | | `engine.editor.addUndoStep()` | Undo | Commit the completed layout swap as one undoable operation. | ## Next Steps Now that you can create collages with layouts, explore these related guides: - [Templates Overview](https://img.ly/docs/cesdk/mac-catalyst/create-templates/overview-4ebe30/) — Work with templates instead of layouts - [Panel](#broken-link-7ce1ee) — Show or hide side panels in an editor UI - [Insert Images](https://img.ly/docs/cesdk/mac-catalyst/insert-media/images-63848a/) — Manage image blocks and fills - [Load a Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/) — Load and save scenes - [Content JSON Schema](https://img.ly/docs/cesdk/mac-catalyst/import-media/content-json-schema-a7b3d2/) — Build custom asset libraries for layouts --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Group and Ungroup Objects" description: "Group multiple blocks to move, scale, and transform them as a single unit; ungroup to edit them individually." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/group-and-ungroup-62565a/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Group and Ungroup Objects](https://img.ly/docs/cesdk/mac-catalyst/create-composition/group-and-ungroup-62565a/) --- ```swift file=@cesdk_swift_examples/engine-guides-grouping/Grouping.swift reference-only import IMGLYEngine @MainActor func grouping(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Create a graphic block with a colored rectangle shape let block1 = try engine.block.create(.graphic) try engine.block.setShape(block1, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block1, value: 120) try engine.block.setHeight(block1, value: 120) try engine.block.setPositionX(block1, value: 200) try engine.block.setPositionY(block1, value: 240) let fill1 = try engine.block.createFill(.color) try engine.block.setColor(fill1, property: "fill/color/value", color: .rgba(r: 0.4, g: 0.6, b: 0.9, a: 1.0)) try engine.block.setFill(block1, fill: fill1) try engine.block.appendChild(to: page, child: block1) // Create two more blocks for grouping let block2 = try engine.block.create(.graphic) try engine.block.setShape(block2, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block2, value: 120) try engine.block.setHeight(block2, value: 120) try engine.block.setPositionX(block2, value: 340) try engine.block.setPositionY(block2, value: 240) let fill2 = try engine.block.createFill(.color) try engine.block.setColor(fill2, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.5, b: 0.4, a: 1.0)) try engine.block.setFill(block2, fill: fill2) try engine.block.appendChild(to: page, child: block2) let block3 = try engine.block.create(.graphic) try engine.block.setShape(block3, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block3, value: 120) try engine.block.setHeight(block3, value: 120) try engine.block.setPositionX(block3, value: 480) try engine.block.setPositionY(block3, value: 240) let fill3 = try engine.block.createFill(.color) try engine.block.setColor(fill3, property: "fill/color/value", color: .rgba(r: 0.5, g: 0.8, b: 0.5, a: 1.0)) try engine.block.setFill(block3, fill: fill3) try engine.block.appendChild(to: page, child: block3) // Check if the blocks can be grouped together let canGroup = try engine.block.isGroupable([block1, block2, block3]) print("Blocks can be grouped:", canGroup) // Group the blocks together if canGroup { let groupID = try engine.block.group([block1, block2, block3]) print("Created group with ID:", groupID) // Select the group to show it in the UI try engine.block.setSelected(groupID, selected: true) // Enter the group to select individual members try engine.block.enterGroup(groupID) // Select a specific member within the group try engine.block.setSelected(block2, selected: true) print("Selected member inside group") // Exit the group to return selection to the parent group try engine.block.exitGroup(block2) print("Exited group, group is now selected") // Find all groups in the scene let allGroups = try engine.block.find(byType: .group) print("Number of groups in scene:", allGroups.count) // Check the type of the group block let groupType = try engine.block.getType(groupID) print("Group block type:", groupType) // Get the members of the group let members = try engine.block.getChildren(groupID) print("Group has", members.count, "members") // Ungroup the blocks to make them independent again try engine.block.ungroup(groupID) print("Ungrouped blocks") // Verify blocks are no longer in a group let groupsAfterUngroup = try engine.block.find(byType: .group) print("Groups after ungrouping:", groupsAfterUngroup.count) // Re-group for the final display let finalGroup = try engine.block.group([block1, block2, block3]) try engine.block.setSelected(finalGroup, selected: true) } try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) } ``` Group multiple blocks to move, scale, and transform them as a single unit; ungroup to edit them individually. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-grouping) Groups let you treat multiple blocks as a cohesive unit. Grouped blocks move, scale, and rotate together while maintaining their relative positions. Groups can contain other groups, enabling hierarchical compositions. This guide covers how to check if blocks can be grouped, create and dissolve groups, navigate into groups to select individual members, and find existing groups in a scene. ## Understanding Groups Groups are blocks with type `.group` that contain child blocks as members. Transformations applied to a group affect all members proportionally — position, scale, and rotation cascade to all children. Groups can be nested, meaning a group can contain other groups. This enables complex hierarchical structures where multiple logical units can be combined and manipulated together. > **Note:** **What cannot be grouped*** Scene blocks cannot be grouped > * A block cannot be grouped with a group it sits inside, which would make that group a child of itself ## Create the Blocks We first create several graphic blocks that we'll group together. Each block has a different color fill to make them visually distinct. ```swift highlight-grouping-createBlocks // Create a graphic block with a colored rectangle shape let block1 = try engine.block.create(.graphic) try engine.block.setShape(block1, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block1, value: 120) try engine.block.setHeight(block1, value: 120) try engine.block.setPositionX(block1, value: 200) try engine.block.setPositionY(block1, value: 240) let fill1 = try engine.block.createFill(.color) try engine.block.setColor(fill1, property: "fill/color/value", color: .rgba(r: 0.4, g: 0.6, b: 0.9, a: 1.0)) try engine.block.setFill(block1, fill: fill1) try engine.block.appendChild(to: page, child: block1) ``` The remaining two blocks are created with the same pattern and appended to the page. ## Check If Blocks Can Be Grouped Before grouping, verify that the selected blocks can be grouped using `engine.block.isGroupable(_:)`. This method returns `true` if all blocks can be grouped together, or `false` if any block is a scene or already belongs to a group. ```swift highlight-grouping-checkGroupable // Check if the blocks can be grouped together let canGroup = try engine.block.isGroupable([block1, block2, block3]) print("Blocks can be grouped:", canGroup) ``` ## Create a Group Use `engine.block.group(_:)` to combine multiple blocks into a new group. The method returns the ID of the newly created group block. The group inherits the combined bounding box of its members. ```swift highlight-grouping-createGroup // Group the blocks together if canGroup { let groupID = try engine.block.group([block1, block2, block3]) print("Created group with ID:", groupID) // Select the group to show it in the UI try engine.block.setSelected(groupID, selected: true) ``` ## Navigate Group Selection CE.SDK provides methods to navigate into and out of groups while editing. ### Enter a Group When a group is selected, use `engine.block.enterGroup(_:)` to enter editing mode for that group. This allows you to select and modify individual members within the group. ```swift highlight-grouping-enterGroup // Enter the group to select individual members try engine.block.enterGroup(groupID) // Select a specific member within the group try engine.block.setSelected(block2, selected: true) print("Selected member inside group") ``` ### Exit a Group When editing a member inside a group, use `engine.block.exitGroup(_:)` to return selection to the parent group. This method takes a member block ID and selects its parent group. ```swift highlight-grouping-exitGroup // Exit the group to return selection to the parent group try engine.block.exitGroup(block2) print("Exited group, group is now selected") ``` ## Find and Inspect Groups Discover groups in a scene and inspect their contents using `engine.block.find(byType:)`, `engine.block.getType(_:)`, and `engine.block.getChildren(_:)`. ```swift highlight-grouping-findGroups // Find all groups in the scene let allGroups = try engine.block.find(byType: .group) print("Number of groups in scene:", allGroups.count) // Check the type of the group block let groupType = try engine.block.getType(groupID) print("Group block type:", groupType) // Get the members of the group let members = try engine.block.getChildren(groupID) print("Group has", members.count, "members") ``` Use `engine.block.find(byType: .group)` to get all group blocks in the current scene. Use `engine.block.getType(_:)` to check if a specific block is a group (returns `"//ly.img.ubq/group"`). Use `engine.block.getChildren(_:)` to get the member blocks of a group. ## Ungroup Blocks Use `engine.block.ungroup(_:)` to dissolve a group and release its children back to the parent container. The children maintain their current positions in the scene. ```swift highlight-grouping-ungroup // Ungroup the blocks to make them independent again try engine.block.ungroup(groupID) print("Ungrouped blocks") // Verify blocks are no longer in a group let groupsAfterUngroup = try engine.block.find(byType: .group) print("Groups after ungrouping:", groupsAfterUngroup.count) ``` ## API Reference | Method | Description | | --- | --- | | `engine.block.isGroupable(_:)` | Check if blocks can be grouped together | | `engine.block.group(_:)` | Create a group from multiple blocks | | `engine.block.ungroup(_:)` | Dissolve a group and release its children | | `engine.block.enterGroup(_:)` | Enter group editing mode (select member) | | `engine.block.exitGroup(_:)` | Exit group editing mode (select parent group) | | `engine.block.find(byType:)` | Find all blocks of a specific type | | `engine.block.getType(_:)` | Get the type string of a block | | `engine.block.getParent(_:)` | Get the parent block | | `engine.block.getChildren(_:)` | Get child blocks of a container | ## Next Steps - [Layer Management](https://img.ly/docs/cesdk/mac-catalyst/create-composition/layer-management-18f07a/) — Control z-order and visibility of blocks - [Position and Align](https://img.ly/docs/cesdk/mac-catalyst/create-composition/position-and-align-cc6b6a/) — Arrange blocks precisely on the canvas - [Lock Design](https://img.ly/docs/cesdk/mac-catalyst/create-composition/lock-design-0a81de/) — Prevent modifications to specific elements --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Layer Management" description: "Organize design elements using a layer stack for precise control over stacking and visibility." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/layer-management-18f07a/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Layers](https://img.ly/docs/cesdk/mac-catalyst/create-composition/layer-management-18f07a/) --- ```swift file=@cesdk_swift_examples/engine-guides-layer-management/LayerManagement.swift reference-only import Foundation import IMGLYEngine @MainActor func layerManagement(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Create a red rectangle let redRect = try engine.block.create(.graphic) try engine.block.setShape(redRect, shape: engine.block.createShape(.rect)) let redFill = try engine.block.createFill(.color) try engine.block.setFill(redRect, fill: redFill) try engine.block.setColor(redFill, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.2, b: 0.2, a: 1.0)) try engine.block.setWidth(redRect, value: 180) try engine.block.setHeight(redRect, value: 180) try engine.block.setPositionX(redRect, value: 220) try engine.block.setPositionY(redRect, value: 120) // Create a green rectangle let greenRect = try engine.block.create(.graphic) try engine.block.setShape(greenRect, shape: engine.block.createShape(.rect)) let greenFill = try engine.block.createFill(.color) try engine.block.setFill(greenRect, fill: greenFill) try engine.block.setColor(greenFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.8, b: 0.2, a: 1.0)) try engine.block.setWidth(greenRect, value: 180) try engine.block.setHeight(greenRect, value: 180) try engine.block.setPositionX(greenRect, value: 280) try engine.block.setPositionY(greenRect, value: 180) // Create a blue rectangle let blueRect = try engine.block.create(.graphic) try engine.block.setShape(blueRect, shape: engine.block.createShape(.rect)) let blueFill = try engine.block.createFill(.color) try engine.block.setFill(blueRect, fill: blueFill) try engine.block.setColor(blueFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0)) try engine.block.setWidth(blueRect, value: 180) try engine.block.setHeight(blueRect, value: 180) try engine.block.setPositionX(blueRect, value: 340) try engine.block.setPositionY(blueRect, value: 240) // Add blocks to the page — last appended is on top try engine.block.appendChild(to: page, child: redRect) try engine.block.appendChild(to: page, child: greenRect) try engine.block.appendChild(to: page, child: blueRect) // Get the parent of a block let parent = try engine.block.getParent(redRect) print("Parent of red rectangle:", parent as Any) // Get all children of the page let children = try engine.block.getChildren(page) print("Page children (in render order):", children) // Insert a new block at a specific position (index 0 = back) let yellowRect = try engine.block.create(.graphic) try engine.block.setShape(yellowRect, shape: engine.block.createShape(.rect)) let yellowFill = try engine.block.createFill(.color) try engine.block.setFill(yellowRect, fill: yellowFill) try engine.block.setColor(yellowFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.85, b: 0.2, a: 1.0)) try engine.block.setWidth(yellowRect, value: 180) try engine.block.setHeight(yellowRect, value: 180) try engine.block.setPositionX(yellowRect, value: 160) try engine.block.setPositionY(yellowRect, value: 60) try engine.block.insertChild(into: page, child: yellowRect, at: 0) // Bring the red rectangle to the front try engine.block.bringToFront(redRect) print("Red rectangle brought to front") // Send the blue rectangle to the back try engine.block.sendToBack(blueRect) print("Blue rectangle sent to back") // Move the green rectangle forward one layer try engine.block.bringForward(greenRect) print("Green rectangle moved forward") // Move the yellow rectangle backward one layer try engine.block.sendBackward(yellowRect) print("Yellow rectangle moved backward") // Check and toggle visibility let isVisible = try engine.block.isVisible(blueRect) print("Blue rectangle visible:", isVisible) // Hide the blue rectangle temporarily try engine.block.setVisible(blueRect, visible: false) print("Blue rectangle hidden") // Show it again for the final composition try engine.block.setVisible(blueRect, visible: true) print("Blue rectangle shown again") // Duplicate a block let duplicateGreen = try engine.block.duplicate(greenRect) try engine.block.setPositionX(duplicateGreen, value: 400) try engine.block.setPositionY(duplicateGreen, value: 300) // Change the duplicate's color to purple let purpleFill = try engine.block.createFill(.color) try engine.block.setFill(duplicateGreen, fill: purpleFill) try engine.block.setColor(purpleFill, property: "fill/color/value", color: .rgba(r: 0.6, g: 0.2, b: 0.8, a: 1.0)) print("Green rectangle duplicated") // Check if a block is valid before operations let isValidBefore = engine.block.isValid(yellowRect) print("Yellow rectangle valid before destroy:", isValidBefore) // Remove a block from the scene try engine.block.destroy(yellowRect) print("Yellow rectangle destroyed") // Check validity after destruction let isValidAfter = engine.block.isValid(yellowRect) print("Yellow rectangle valid after destroy:", isValidAfter) try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) } ``` Organize design elements in CE.SDK using a hierarchical layer stack to control stacking order, visibility, and element relationships. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-layer-management) Design elements in CE.SDK are organized in a hierarchical parent-child structure. Children of a block are rendered in order, with the last child appearing on top. This layer stack model gives you precise control over how elements overlap and interact visually. This guide covers how to navigate the block hierarchy, reorder elements in the layer stack, toggle visibility, and manage block lifecycles through duplication and deletion. ## Creating Visual Blocks To demonstrate layer ordering, we create colored rectangles that overlap on the canvas. Each block is created using `engine.block.create(.graphic)` and configured with a shape, fill color, dimensions, and position. ```swift highlight-layerManagement-createBlock // Create a red rectangle let redRect = try engine.block.create(.graphic) try engine.block.setShape(redRect, shape: engine.block.createShape(.rect)) let redFill = try engine.block.createFill(.color) try engine.block.setFill(redRect, fill: redFill) try engine.block.setColor(redFill, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.2, b: 0.2, a: 1.0)) try engine.block.setWidth(redRect, value: 180) try engine.block.setHeight(redRect, value: 180) try engine.block.setPositionX(redRect, value: 220) try engine.block.setPositionY(redRect, value: 120) ``` ## Navigating the Block Hierarchy CE.SDK organizes blocks in a parent-child tree. Every block can have one parent and multiple children. Understanding this hierarchy is essential for programmatic layer management. ### Getting a Block's Parent Retrieve the parent of any block using `engine.block.getParent(_:)`. This returns the parent's block ID, or `nil` if the block has no parent. ```swift highlight-layerManagement-getParent // Get the parent of a block let parent = try engine.block.getParent(redRect) print("Parent of red rectangle:", parent as Any) ``` ### Listing Child Blocks Get all direct children of a block using `engine.block.getChildren(_:)`. Children are returned sorted in their rendering order — the last child renders in front of other children. ```swift highlight-layerManagement-getChildren // Get all children of the page let children = try engine.block.getChildren(page) print("Page children (in render order):", children) ``` This method is useful for iterating over all elements on a page or within a group. ## Adding and Positioning Blocks When you create a new block, it exists independently until you add it to the hierarchy. There are two ways to attach blocks to a parent: appending to the end or inserting at a specific position. ### Appending Blocks Add a block as the last child of a parent using `engine.block.appendChild(to:child:)`. Since the last child renders on top, the appended block becomes the topmost element. ```swift highlight-layerManagement-appendChild // Add blocks to the page — last appended is on top try engine.block.appendChild(to: page, child: redRect) try engine.block.appendChild(to: page, child: greenRect) try engine.block.appendChild(to: page, child: blueRect) ``` When you append multiple blocks in sequence, each new block appears in front of the previous ones. ### Inserting at a Specific Position Insert a block at a specific index in the layer stack using `engine.block.insertChild(into:child:at:)`. Index 0 places the block at the back, behind all other children. ```swift highlight-layerManagement-insertChild // Insert a new block at a specific position (index 0 = back) let yellowRect = try engine.block.create(.graphic) try engine.block.setShape(yellowRect, shape: engine.block.createShape(.rect)) let yellowFill = try engine.block.createFill(.color) try engine.block.setFill(yellowRect, fill: yellowFill) try engine.block.setColor(yellowFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.85, b: 0.2, a: 1.0)) try engine.block.setWidth(yellowRect, value: 180) try engine.block.setHeight(yellowRect, value: 180) try engine.block.setPositionX(yellowRect, value: 160) try engine.block.setPositionY(yellowRect, value: 60) try engine.block.insertChild(into: page, child: yellowRect, at: 0) ``` ### Reparenting Blocks When you add a block to a new parent using `appendChild(to:child:)` or `insertChild(into:child:at:)`, it is automatically removed from its previous parent. This makes reparenting straightforward without needing to manually detach blocks first. ## Changing Z-Order Once blocks are in the hierarchy, you can change their stacking order without removing and re-adding them. CE.SDK provides four methods for z-order manipulation. ### Bring to Front Move an element to the top of its siblings using `engine.block.bringToFront(_:)`. This gives the block the highest stacking order among its siblings. ```swift highlight-layerManagement-bringToFront // Bring the red rectangle to the front try engine.block.bringToFront(redRect) print("Red rectangle brought to front") ``` ### Send to Back Move an element behind all its siblings using `engine.block.sendToBack(_:)`. This gives the block the lowest stacking order among its siblings. ```swift highlight-layerManagement-sendToBack // Send the blue rectangle to the back try engine.block.sendToBack(blueRect) print("Blue rectangle sent to back") ``` ### Move Forward One Layer Move an element one position forward using `engine.block.bringForward(_:)`. This swaps the block with its immediate sibling in front. ```swift highlight-layerManagement-bringForward // Move the green rectangle forward one layer try engine.block.bringForward(greenRect) print("Green rectangle moved forward") ``` ### Move Backward One Layer Move an element one position backward using `engine.block.sendBackward(_:)`. This swaps the block with its immediate sibling behind. ```swift highlight-layerManagement-sendBackward // Move the yellow rectangle backward one layer try engine.block.sendBackward(yellowRect) print("Yellow rectangle moved backward") ``` These incremental operations are useful for fine-tuning layer order without jumping to extremes. ## Controlling Visibility Visibility allows you to temporarily hide elements without removing them from the scene. Hidden elements remain in the hierarchy and preserve their properties but are not rendered. Query the current visibility state using `engine.block.isVisible(_:)` and change it using `engine.block.setVisible(_:visible:)`. ```swift highlight-layerManagement-visibility // Check and toggle visibility let isVisible = try engine.block.isVisible(blueRect) print("Blue rectangle visible:", isVisible) // Hide the blue rectangle temporarily try engine.block.setVisible(blueRect, visible: false) print("Blue rectangle hidden") // Show it again for the final composition try engine.block.setVisible(blueRect, visible: true) print("Blue rectangle shown again") ``` Visibility is useful for creating before/after comparisons, hiding elements during editing, or implementing show/hide functionality in your application. ## Managing Block Lifecycle CE.SDK provides methods for duplicating blocks to create copies and destroying blocks to remove them permanently. ### Duplicating Blocks Create a copy of a block and all its children using `engine.block.duplicate(_:)`. By default, the duplicate is attached to the same parent as the original. ```swift highlight-layerManagement-duplicate // Duplicate a block let duplicateGreen = try engine.block.duplicate(greenRect) try engine.block.setPositionX(duplicateGreen, value: 400) try engine.block.setPositionY(duplicateGreen, value: 300) // Change the duplicate's color to purple let purpleFill = try engine.block.createFill(.color) try engine.block.setFill(duplicateGreen, fill: purpleFill) try engine.block.setColor(purpleFill, property: "fill/color/value", color: .rgba(r: 0.6, g: 0.2, b: 0.8, a: 1.0)) print("Green rectangle duplicated") ``` The duplicated block is positioned at the same location as the original. Reposition it to make it visible as a separate element. ### Checking Block Validity Before performing operations on a block, verify it still exists using `engine.block.isValid(_:)`. A block becomes invalid after it has been destroyed. ```swift highlight-layerManagement-isValid // Check if a block is valid before operations let isValidBefore = engine.block.isValid(yellowRect) print("Yellow rectangle valid before destroy:", isValidBefore) ``` ### Removing Blocks Permanently remove a block and all its children from the scene using `engine.block.destroy(_:)`. ```swift highlight-layerManagement-destroy // Remove a block from the scene try engine.block.destroy(yellowRect) print("Yellow rectangle destroyed") // Check validity after destruction let isValidAfter = engine.block.isValid(yellowRect) print("Yellow rectangle valid after destroy:", isValidAfter) ``` After destruction, any reference to the block becomes invalid. Attempting to use an invalid block ID results in errors. ## Framing the Result After making layer changes, zoom to fit the page in the viewport so the composition is clearly visible. ```swift highlight-layerManagement-zoom try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) ``` ## Troubleshooting **Block not visible after appendChild**: The block may be behind other elements. Use `engine.block.bringToFront(_:)` or adjust the insert index to control stacking order. **getParent returns nil**: The block is not attached to any parent. Use `engine.block.appendChild(to:child:)` or `engine.block.insertChild(into:child:at:)` to attach it to a page or container. **Changes not reflected**: The block handle may be invalid. Check with `engine.block.isValid(_:)` before performing operations. **Z-order not updating**: Verify you're operating on the correct block ID and that the block is in the expected parent context. **Duplicate not appearing**: If `attachToParent` is set to false, the duplicate won't be attached automatically. Set it to true or manually attach the duplicate to a parent. ## API Reference | Method | Category | Description | | --- | --- | --- | | `engine.block.getParent(_:)` | Hierarchy | Get the parent block of a given block | | `engine.block.getChildren(_:)` | Hierarchy | Get all child blocks in rendering order | | `engine.block.appendChild(to:child:)` | Hierarchy | Append a block as the last child | | `engine.block.insertChild(into:child:at:)` | Hierarchy | Insert a block at a specific position | | `engine.block.bringToFront(_:)` | Z-Order | Bring a block to the front of its siblings | | `engine.block.sendToBack(_:)` | Z-Order | Send a block to the back of its siblings | | `engine.block.bringForward(_:)` | Z-Order | Move a block one position forward | | `engine.block.sendBackward(_:)` | Z-Order | Move a block one position backward | | `engine.block.isVisible(_:)` | Visibility | Check if a block is visible | | `engine.block.setVisible(_:visible:)` | Visibility | Set the visibility of a block | | `engine.block.duplicate(_:)` | Lifecycle | Duplicate a block and its children | | `engine.block.destroy(_:)` | Lifecycle | Remove a block and its children | | `engine.block.isValid(_:)` | Lifecycle | Check if a block handle is valid | ## Next Steps - [Grouping](https://img.ly/docs/cesdk/mac-catalyst/create-composition/group-and-ungroup-62565a/) — Group multiple blocks to move or transform them together - [Position and Align](https://img.ly/docs/cesdk/mac-catalyst/create-composition/position-and-align-cc6b6a/) — Precisely position elements on the canvas - [Multi-Page Layouts](https://img.ly/docs/cesdk/mac-catalyst/create-composition/multi-page-4d2b50/) — Work with multiple pages in a single scene --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Design a Layout" description: "Documentation for Design a Layout" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/layout-b66311/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Design a Layout](https://img.ly/docs/cesdk/mac-catalyst/create-composition/layout-b66311/) --- ```swift file=@cesdk_swift_examples/engine-guides-layout/Layout.swift reference-only import Foundation import IMGLYEngine @MainActor func layout(engine: Engine) async throws { // Create a scene with VerticalStack layout. Pages appended to the stack // container are arranged top-to-bottom automatically. try engine.scene.create(sceneLayout: .verticalStack) // Get the stack container that was created with the scene. let stacks = try engine.block.find(byType: .stack) let stack = stacks[0] // Create two pages that will stack vertically. let page1 = try engine.block.create(.page) try engine.block.setWidth(page1, value: 400) try engine.block.setHeight(page1, value: 300) try engine.block.appendChild(to: stack, child: page1) let page2 = try engine.block.create(.page) try engine.block.setWidth(page2, value: 400) try engine.block.setHeight(page2, value: 300) try engine.block.appendChild(to: stack, child: page2) // Configure spacing between stacked pages. try engine.block.setFloat(stack, property: "stack/spacing", value: 20) try engine.block.setBool(stack, property: "stack/spacingInScreenspace", value: true) // Resolve sample assets against the bundled asset base URL. let baseURL = try engine.guidesBaseURL // Add an image block to the first page. let block1 = try engine.block.create(.graphic) let shape1 = try engine.block.createShape(.rect) try engine.block.setShape(block1, shape: shape1) try engine.block.setWidth(block1, value: 350) try engine.block.setHeight(block1, value: 250) try engine.block.setPositionX(block1, value: 25) try engine.block.setPositionY(block1, value: 25) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(block1, fill: imageFill) try engine.block.appendChild(to: page1, child: block1) // Add a colored rectangle to the second page. let block2 = try engine.block.create(.graphic) let shape2 = try engine.block.createShape(.rect) try engine.block.setShape(block2, shape: shape2) try engine.block.setWidth(block2, value: 350) try engine.block.setHeight(block2, value: 250) try engine.block.setPositionX(block2, value: 25) try engine.block.setPositionY(block2, value: 25) let colorFill = try engine.block.createFill(.color) try engine.block.setColor( colorFill, property: "fill/color/value", color: .rgba(r: 0.3, g: 0.6, b: 0.9, a: 1.0), ) try engine.block.setFill(block2, fill: colorFill) try engine.block.appendChild(to: page2, child: block2) // Switch to a horizontal stack. Existing pages reposition left-to-right. try engine.scene.setLayout(.horizontalStack) // Verify the layout type. let currentLayout = try engine.scene.getLayout() print("Current layout:", currentLayout) // Append a new page to the existing stack. It snaps to the end with the // configured spacing. let page3 = try engine.block.create(.page) try engine.block.setWidth(page3, value: 400) try engine.block.setHeight(page3, value: 300) try engine.block.appendChild(to: stack, child: page3) // Add content to the new page. let block3 = try engine.block.create(.graphic) let shape3 = try engine.block.createShape(.rect) try engine.block.setShape(block3, shape: shape3) try engine.block.setWidth(block3, value: 350) try engine.block.setHeight(block3, value: 250) try engine.block.setPositionX(block3, value: 25) try engine.block.setPositionY(block3, value: 25) let fill3 = try engine.block.createFill(.color) try engine.block.setColor( fill3, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.5, b: 0.3, a: 1.0), ) try engine.block.setFill(block3, fill: fill3) try engine.block.appendChild(to: page3, child: block3) // Move page3 to the first position using insertChild. try engine.block.insertChild(into: stack, child: page3, at: 0) // Verify the new order. let pageOrder = try engine.block.getChildren(stack) print("Page order after reordering:", pageOrder) // Update the spacing between stacked pages. try engine.block.setFloat(stack, property: "stack/spacing", value: 40) // Verify the spacing value. let updatedSpacing = try engine.block.getFloat(stack, property: "stack/spacing") print("Updated spacing:", updatedSpacing) // Switch back to a free layout to position pages manually. try engine.scene.setLayout(.free) // Position a page directly — stacks no longer manage placement. let pages = try engine.block.find(byType: .page) let page = pages[0] try engine.block.setPositionX(page, value: 100) try engine.block.setPositionY(page, value: 200) } ``` Create structured compositions using stack layouts that automatically arrange pages vertically or horizontally with consistent spacing. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-layout) Stack layouts arrange pages automatically with consistent spacing. Vertical stacks arrange pages top-to-bottom, while horizontal stacks arrange them left-to-right. This eliminates manual positioning for compositions like photo collages, product catalogs, or social media carousels. This guide covers how to: - Create vertical and horizontal stack layouts - Add pages and blocks to stacks - Configure spacing between stacked pages - Reorder pages within a stack - Switch between stack and free layouts ## Create a Vertical Stack Layout Vertical stacks arrange pages from top to bottom. Create a scene with `.verticalStack` layout, then append pages to the stack container. ```swift highlight-layout-verticalStack // Create a scene with VerticalStack layout. Pages appended to the stack // container are arranged top-to-bottom automatically. try engine.scene.create(sceneLayout: .verticalStack) // Get the stack container that was created with the scene. let stacks = try engine.block.find(byType: .stack) let stack = stacks[0] // Create two pages that will stack vertically. let page1 = try engine.block.create(.page) try engine.block.setWidth(page1, value: 400) try engine.block.setHeight(page1, value: 300) try engine.block.appendChild(to: stack, child: page1) let page2 = try engine.block.create(.page) try engine.block.setWidth(page2, value: 400) try engine.block.setHeight(page2, value: 300) try engine.block.appendChild(to: stack, child: page2) // Configure spacing between stacked pages. try engine.block.setFloat(stack, property: "stack/spacing", value: 20) try engine.block.setBool(stack, property: "stack/spacingInScreenspace", value: true) ``` When you create a scene with `.verticalStack`, CE.SDK automatically adds a stack container. Pages appended to this container position themselves with the configured spacing. The `stack/spacingInScreenspace` property keeps spacing visually consistent at any zoom level. The distinction applies to the canvas preview only. Exports always interpret the spacing in design units, whatever the property is set to. Switching the property never converts the number. A spacing of `40` stays `40` and is reinterpreted in the other space, which changes how far apart the pages appear. ## Add Blocks to Pages Each page can contain multiple blocks. Create blocks with a shape and fill, position them inside the page, then append them as children. ```swift highlight-layout-addBlocks // Add an image block to the first page. let block1 = try engine.block.create(.graphic) let shape1 = try engine.block.createShape(.rect) try engine.block.setShape(block1, shape: shape1) try engine.block.setWidth(block1, value: 350) try engine.block.setHeight(block1, value: 250) try engine.block.setPositionX(block1, value: 25) try engine.block.setPositionY(block1, value: 25) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(block1, fill: imageFill) try engine.block.appendChild(to: page1, child: block1) // Add a colored rectangle to the second page. let block2 = try engine.block.create(.graphic) let shape2 = try engine.block.createShape(.rect) try engine.block.setShape(block2, shape: shape2) try engine.block.setWidth(block2, value: 350) try engine.block.setHeight(block2, value: 250) try engine.block.setPositionX(block2, value: 25) try engine.block.setPositionY(block2, value: 25) let colorFill = try engine.block.createFill(.color) try engine.block.setColor( colorFill, property: "fill/color/value", color: .rgba(r: 0.3, g: 0.6, b: 0.9, a: 1.0), ) try engine.block.setFill(block2, fill: colorFill) try engine.block.appendChild(to: page2, child: block2) ``` Graphic blocks require both a shape and a fill to be visible. Use an image fill with `fill/image/imageFileURI` for image content, or a color fill for solid colors. Position blocks inside their parent page with `setPositionX` and `setPositionY`. ## Switch to Horizontal Layout Change the layout direction at any time with `setLayout`. Horizontal stacks arrange pages left-to-right instead of top-to-bottom. ```swift highlight-layout-horizontalStack // Switch to a horizontal stack. Existing pages reposition left-to-right. try engine.scene.setLayout(.horizontalStack) // Verify the layout type. let currentLayout = try engine.scene.getLayout() print("Current layout:", currentLayout) ``` Horizontal layouts suit carousels, timelines, and horizontal galleries. Existing pages reposition automatically when you change the layout type. ## Add Pages to Existing Stacks Append new pages to an existing stack at any time. Pages snap to the end of the stack with the configured spacing. ```swift highlight-layout-addPage // Append a new page to the existing stack. It snaps to the end with the // configured spacing. let page3 = try engine.block.create(.page) try engine.block.setWidth(page3, value: 400) try engine.block.setHeight(page3, value: 300) try engine.block.appendChild(to: stack, child: page3) // Add content to the new page. let block3 = try engine.block.create(.graphic) let shape3 = try engine.block.createShape(.rect) try engine.block.setShape(block3, shape: shape3) try engine.block.setWidth(block3, value: 350) try engine.block.setHeight(block3, value: 250) try engine.block.setPositionX(block3, value: 25) try engine.block.setPositionY(block3, value: 25) let fill3 = try engine.block.createFill(.color) try engine.block.setColor( fill3, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.5, b: 0.3, a: 1.0), ) try engine.block.setFill(block3, fill: fill3) try engine.block.appendChild(to: page3, child: block3) ``` The stack container handles positioning automatically. You can populate the new page with content before or after appending it. ## Reorder Pages Change page order with `insertChild` to place a page at a specific index inside the stack. ```swift highlight-layout-reorder // Move page3 to the first position using insertChild. try engine.block.insertChild(into: stack, child: page3, at: 0) // Verify the new order. let pageOrder = try engine.block.getChildren(stack) print("Page order after reordering:", pageOrder) ``` Removing a page from its current slot and reinserting it at a new index moves it to that position. The remaining pages shift to make room. ## Change Stack Spacing Adjust spacing between pages by setting the `stack/spacing` property on the stack block. ```swift highlight-layout-spacing // Update the spacing between stacked pages. try engine.block.setFloat(stack, property: "stack/spacing", value: 40) // Verify the spacing value. let updatedSpacing = try engine.block.getFloat(stack, property: "stack/spacing") print("Updated spacing:", updatedSpacing) ``` Spacing updates take effect immediately and pages reposition automatically. Read the current value back with `getFloat`. ## Switch to Free Layout For manual positioning, switch to `.free`. Pages keep their current positions but stop auto-arranging. ```swift highlight-layout-freeLayout // Switch back to a free layout to position pages manually. try engine.scene.setLayout(.free) // Position a page directly — stacks no longer manage placement. let pages = try engine.block.find(byType: .page) let page = pages[0] try engine.block.setPositionX(page, value: 100) try engine.block.setPositionY(page, value: 200) ``` Free layout gives full control over page positions. Use this when you need precise placement that stack layouts cannot provide. ## Troubleshooting **Pages not arranging automatically** — Verify the scene layout is `.verticalStack` or `.horizontalStack` with `getLayout()`. **Spacing not applying** — Set `stack/spacing` on the stack block, not the scene. Use `find(byType: .stack)` to locate the container. **Pages overlapping** — Ensure pages are direct children of the stack container. Nested pages do not auto-arrange. **Can't position manually** — Stack layouts override manual positions. Switch to `.free` for manual control. **Wrong stacking order** — Child order determines position. Use `insertChild(into:child:at:)` to move pages to a specific slot. ## API Reference | Method | Description | |--------|-------------| | `engine.scene.create(sceneLayout:)` | Create a scene with the specified layout (`.free`, `.verticalStack`, `.horizontalStack`). | | `engine.scene.setLayout(_:)` | Change the layout of the current scene. | | `engine.scene.getLayout()` | Get the current scene layout. | | `engine.block.find(byType: .stack)` | Find the stack container block. | | `engine.block.setFloat(_:property:value:)` with `stack/spacing` | Set spacing between stacked pages. | | `engine.block.getFloat(_:property:)` with `stack/spacing` | Get the current spacing value. | | `engine.block.appendChild(to:child:)` | Append a page to the stack. | | `engine.block.insertChild(into:child:at:)` | Insert a page at a specific position. | | `engine.block.getChildren(_:)` | Get child blocks in order. | ## Next Steps - [Auto-resize](https://img.ly/docs/cesdk/mac-catalyst/automation/auto-resize-4c2d58/) — Make blocks fit parent containers - [Manual Positioning](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/move-818dd9/) — Position blocks in free layouts - [Layer Hierarchies](https://img.ly/docs/cesdk/mac-catalyst/create-composition/layer-management-18f07a/) — Organize blocks in hierarchical structures - [Create a Collage](https://img.ly/docs/cesdk/mac-catalyst/create-composition/collage-f7d28d/) — Build photo collages with templates --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Lock Design" description: "Protect design elements from unwanted modifications using CE.SDK's scope-based permission system. Control which properties users can edit at both global and block levels." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/lock-design-0a81de/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Lock Design](https://img.ly/docs/cesdk/mac-catalyst/create-composition/lock-design-0a81de/) --- Protect design elements from unwanted modifications using CE.SDK's scope-based permission system. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-lock-design) CE.SDK uses a two-layer scope system to control editing permissions. Global scopes set defaults for the entire scene, while block-level scopes override when the global setting is `.defer`. This enables flexible permission models from fully locked to selectively editable designs. ```swift file=@cesdk_swift_examples/engine-guides-lock-design/LockDesign.swift reference-only import Foundation import IMGLYEngine @MainActor func lockDesign(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") // Column 1: Fully Locked let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) let imageFill = try engine.block.createFill(.image) try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.setPositionX(imageBlock, value: 30) try engine.block.setPositionY(imageBlock, value: 100) try engine.block.setWidth(imageBlock, value: 220) try engine.block.setHeight(imageBlock, value: 165) try engine.block.appendChild(to: page, child: imageBlock) // Column 2: Text Editing Only let textBlock = try engine.block.create(.text) try engine.block.setString(textBlock, property: "text/text", value: "Edit Me") try engine.block.setFloat(textBlock, property: "text/fontSize", value: 72) try engine.block.setPositionX(textBlock, value: 290) try engine.block.setPositionY(textBlock, value: 100) try engine.block.setWidth(textBlock, value: 220) try engine.block.setHeight(textBlock, value: 165) try engine.block.appendChild(to: page, child: textBlock) // Column 3: Image Replace Only let placeholderBlock = try engine.block.create(.graphic) try engine.block.setShape(placeholderBlock, shape: engine.block.createShape(.rect)) let placeholderFill = try engine.block.createFill(.image) try engine.block.setURL(placeholderFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(placeholderBlock, fill: placeholderFill) try engine.block.setPositionX(placeholderBlock, value: 550) try engine.block.setPositionY(placeholderBlock, value: 100) try engine.block.setWidth(placeholderBlock, value: 220) try engine.block.setHeight(placeholderBlock, value: 165) try engine.block.appendChild(to: page, child: placeholderBlock) // Lock the entire design by setting all scopes to .deny let scopes = try engine.editor.findAllScopes() for scope in scopes { try engine.editor.setGlobalScope(key: scope, value: .deny) } // Enable selection for specific blocks try engine.editor.setGlobalScope(key: "editor/select", value: .defer) try engine.block.setScopeEnabled(textBlock, key: "editor/select", enabled: true) try engine.block.setScopeEnabled(placeholderBlock, key: "editor/select", enabled: true) // Enable text editing on the text block try engine.editor.setGlobalScope(key: "text/edit", value: .defer) try engine.editor.setGlobalScope(key: "text/character", value: .defer) try engine.block.setScopeEnabled(textBlock, key: "text/edit", enabled: true) try engine.block.setScopeEnabled(textBlock, key: "text/character", enabled: true) // Enable image replacement on the placeholder block try engine.editor.setGlobalScope(key: "fill/change", value: .defer) try engine.block.setScopeEnabled(placeholderBlock, key: "fill/change", enabled: true) // Check if operations are permitted on blocks let canEditText = try engine.block.isAllowedByScope(textBlock, key: "text/edit") let canMoveImage = try engine.block.isAllowedByScope(imageBlock, key: "layer/move") let canReplacePlaceholder = try engine.block.isAllowedByScope(placeholderBlock, key: "fill/change") print("Permission status:") print("- Can edit text:", canEditText) // true print("- Can move locked image:", canMoveImage) // false print("- Can replace placeholder:", canReplacePlaceholder) // true // Discover all available scopes let allScopes = try engine.editor.findAllScopes() print("Available scopes:", allScopes) // Check global scope settings let textEditGlobal = try engine.editor.getGlobalScope(key: "text/edit") let layerMoveGlobal = try engine.editor.getGlobalScope(key: "layer/move") print("Global text/edit:", textEditGlobal) // .defer print("Global layer/move:", layerMoveGlobal) // .deny // Check block-level scope settings let textEditEnabled = try engine.block.isScopeEnabled(textBlock, key: "text/edit") print("Text block text/edit enabled:", textEditEnabled) // true // Select the text block to demonstrate editability try engine.block.select(textBlock) } ``` This guide covers how to lock entire designs, selectively enable specific editing capabilities, and check permissions programmatically. ## Understanding the Scope Permission Model Scopes control what operations users can perform on design elements. CE.SDK combines global scope settings with block-level settings to determine the final permission. | Global Scope | Block Scope | Result | | ------------ | ----------- | --------- | | `.allow` | any | Permitted | | `.deny` | any | Blocked | | `.defer` | enabled | Permitted | | `.defer` | disabled | Blocked | Global scopes have three possible values: - **`.allow`**: The operation is always permitted, regardless of block-level settings - **`.deny`**: The operation is always blocked, regardless of block-level settings - **`.defer`**: The permission depends on the block-level scope setting Block-level scopes are binary: enabled or disabled. They only take effect when the global scope is set to `.defer`. ## Locking an Entire Design To lock all editing operations, iterate through all available scopes and set each to `.deny`. We use `engine.editor.findAllScopes()` to discover all scope names dynamically. ```swift highlight-lockDesign-lockEntireDesign // Lock the entire design by setting all scopes to .deny let scopes = try engine.editor.findAllScopes() for scope in scopes { try engine.editor.setGlobalScope(key: scope, value: .deny) } ``` When all scopes are set to `.deny`, users cannot modify any aspect of the design. This includes selecting, moving, editing text, or changing any visual properties. ## Enabling Selection for Interactive Blocks Before users can interact with any block, you must enable the `editor/select` scope. Without selection, users cannot click on or access any blocks, even if other editing capabilities are enabled. ```swift highlight-lockDesign-enableSelection // Enable selection for specific blocks try engine.editor.setGlobalScope(key: "editor/select", value: .defer) try engine.block.setScopeEnabled(textBlock, key: "editor/select", enabled: true) try engine.block.setScopeEnabled(placeholderBlock, key: "editor/select", enabled: true) ``` Setting the global `editor/select` scope to `.defer` delegates the decision to each block. We then enable selection only on the specific blocks users should be able to interact with. ## Selective Locking Patterns Lock everything first, then selectively enable specific capabilities on chosen blocks. This pattern provides fine-grained control over what users can modify. ### Text-Only Editing To allow users to edit text content while protecting everything else, enable the `text/edit` scope. For text styling changes like font, size, and color, also enable `text/character`. ```swift highlight-lockDesign-textEditing // Enable text editing on the text block try engine.editor.setGlobalScope(key: "text/edit", value: .defer) try engine.editor.setGlobalScope(key: "text/character", value: .defer) try engine.block.setScopeEnabled(textBlock, key: "text/edit", enabled: true) try engine.block.setScopeEnabled(textBlock, key: "text/character", enabled: true) ``` Users can now type new text content in the designated text block but cannot move, resize, or delete it. ### Image Replacement To allow users to swap images while protecting layout and position, enable the `fill/change` scope on placeholder blocks. ```swift highlight-lockDesign-imageReplacement // Enable image replacement on the placeholder block try engine.editor.setGlobalScope(key: "fill/change", value: .defer) try engine.block.setScopeEnabled(placeholderBlock, key: "fill/change", enabled: true) ``` Users can replace the image content but the block's position, dimensions, and other properties remain locked. ## Checking Permissions Verify whether operations are permitted using `engine.block.isAllowedByScope(_:key:)`. This method evaluates both global and block-level settings to return the effective permission state. ```swift highlight-lockDesign-checkPermissions // Check if operations are permitted on blocks let canEditText = try engine.block.isAllowedByScope(textBlock, key: "text/edit") let canMoveImage = try engine.block.isAllowedByScope(imageBlock, key: "layer/move") let canReplacePlaceholder = try engine.block.isAllowedByScope(placeholderBlock, key: "fill/change") print("Permission status:") print("- Can edit text:", canEditText) // true print("- Can move locked image:", canMoveImage) // false print("- Can replace placeholder:", canReplacePlaceholder) // true ``` The distinction between checking methods is: - `isAllowedByScope(_:key:)` returns the **effective permission** after evaluating all scope levels - `isScopeEnabled(_:key:)` returns only the **block-level setting** - `getGlobalScope(key:)` returns only the **global setting** ## Discovering Available Scopes To work with scopes programmatically, you can discover all available scope names and check their current settings. ```swift highlight-lockDesign-getScopes // Discover all available scopes let allScopes = try engine.editor.findAllScopes() print("Available scopes:", allScopes) // Check global scope settings let textEditGlobal = try engine.editor.getGlobalScope(key: "text/edit") let layerMoveGlobal = try engine.editor.getGlobalScope(key: "layer/move") print("Global text/edit:", textEditGlobal) // .defer print("Global layer/move:", layerMoveGlobal) // .deny // Check block-level scope settings let textEditEnabled = try engine.block.isScopeEnabled(textBlock, key: "text/edit") print("Text block text/edit enabled:", textEditEnabled) // true ``` ## Available Scopes Reference | Scope | Description | | ------------------------ | --------------------------------------- | | `layer/move` | Move block position | | `layer/resize` | Resize block dimensions | | `layer/rotate` | Rotate block | | `layer/flip` | Flip block horizontally or vertically | | `layer/crop` | Crop block content | | `layer/opacity` | Change block opacity | | `layer/blendMode` | Change blend mode | | `layer/visibility` | Toggle block visibility | | `layer/clipping` | Change clipping behavior | | `fill/change` | Change fill content | | `fill/changeType` | Change fill type | | `stroke/change` | Change stroke properties | | `shape/change` | Change shape type | | `text/edit` | Edit text content | | `text/character` | Change text styling (font, size, color) | | `appearance/adjustments` | Change color adjustments | | `appearance/filter` | Apply or change filters | | `appearance/effect` | Apply or change effects | | `appearance/blur` | Apply or change blur | | `appearance/shadow` | Apply or change shadows | | `appearance/animation` | Apply or change animations | | `lifecycle/destroy` | Delete the block | | `lifecycle/duplicate` | Duplicate the block | | `editor/add` | Add new blocks | | `editor/select` | Select blocks | ## Next Steps - [Lock Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) - Lock templates for consistent reuse - [Lock Content](https://img.ly/docs/cesdk/mac-catalyst/rules/lock-content-9fa727/) — Lock content using the rules system - [Rules Overview](https://img.ly/docs/cesdk/mac-catalyst/rules/overview-e27832/) - Understand the broader rules system --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Masks and Mattes" description: "Show only part of a block with shape masks or with a track matte from another block." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/masks-and-mattes-aa9c93/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Masks and Mattes](https://img.ly/docs/cesdk/mac-catalyst/create-composition/masks-and-mattes-aa9c93/) --- ```swift file=@cesdk_swift_examples/engine-guides-masks-and-mattes/MasksAndMattes.swift reference-only import Foundation import IMGLYEngine @MainActor func masksAndMattes(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1500) try engine.block.setHeight(page, value: 500) try engine.block.appendChild(to: scene, child: page) // A 400 by 400 square graphic at a given x position func addSquare(x: Float, fill: DesignBlockID) throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: 400) try engine.block.setHeight(block, value: 400) try engine.block.setPositionX(block, value: x) try engine.block.setPositionY(block, value: 50) try engine.block.appendChild(to: page, child: block) return block } let purple = try engine.block.createFill(.color) try engine.block.setColor(purple, property: "fill/color/value", color: .rgba(r: 0.5, g: 0.2, b: 0.9, a: 1)) let ringBlock = try addSquare(x: 50, fill: purple) print(try engine.block.supportsMasks(ringBlock)) // true print(try engine.block.supportsMasks(page)) // false // The ellipse fills the frame of the masked block let outerMask = try engine.block.createMask() try engine.block.setShape(outerMask, shape: engine.block.createShape(.ellipse)) try engine.block.appendMask(ringBlock, maskID: outerMask) // Soften the edge by 20 units and let 80% of the block show through try engine.block.setFloat(outerMask, property: "mask/feather", value: 20) try engine.block.setFloat(outerMask, property: "mask/opacity", value: 0.8) // Shrink a second ellipse by 100 units and subtract it to cut a ring let innerMask = try engine.block.createMask() try engine.block.setShape(innerMask, shape: engine.block.createShape(.ellipse)) try engine.block.setFloat(innerMask, property: "mask/expansion", value: -100) try engine.block.setEnum(innerMask, property: "mask/mode", value: "Subtract") try engine.block.appendMask(ringBlock, maskID: innerMask) let teal = try engine.block.createFill(.color) try engine.block.setColor(teal, property: "fill/color/value", color: .rgba(r: 0, g: 0.6, b: 0.6, a: 1)) let starBlock = try addSquare(x: 550, fill: teal) // An inverted mask keeps the area outside its shape: a star-shaped hole let starMask = try engine.block.createMask() try engine.block.setShape(starMask, shape: engine.block.createShape(.star)) try engine.block.setBool(starMask, property: "mask/inverted", value: true) try engine.block.appendMask(starBlock, maskID: starMask) print(try engine.block.getMasks(ringBlock)) // [outerMask, innerMask] // Removing a mask does not destroy it try engine.block.removeMask(ringBlock, index: 1) print(engine.block.isValid(innerMask)) // true // Insert it again after the outer mask try engine.block.insertMask(ringBlock, maskID: innerMask, index: 1) let orange = try engine.block.createFill(.color) try engine.block.setColor(orange, property: "fill/color/value", color: .rgba(r: 1, g: 0.5, b: 0, a: 1)) let target = try addSquare(x: 1050, fill: orange) // The matte source: a gradient from white at the top to black at the bottom let gradient = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( gradient, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1, g: 1, b: 1, a: 1), stop: 0), GradientColorStop(color: .rgba(r: 0, g: 0, b: 0, a: 1), stop: 1), ], ) let source = try addSquare(x: 1050, fill: gradient) // The target shows where the source is bright. The source is not drawn. try engine.block.setMatte(target, source: source, mode: .luma) let matte = try engine.block.getMatte(target) print(matte == BlockMatte(source: source, mode: .luma)) // true print(try engine.block.getMatte(ringBlock) == nil) // true do { // A block cannot use itself as its matte source try engine.block.setMatte(target, source: target) } catch let error as EngineError { print(error.catalogCode == .blockMatteSourceInvalid) // true } let png = try await engine.block.export(page, mimeType: .png) // SVG export ignores the mask opacity and feather, and draws the matted block whole let svg = try await engine.block.export(page, mimeType: .svg) print("Exported \(png.count) PNG bytes and \(svg.count) SVG bytes") } ``` Show only part of a block with shape masks, or with a track matte that uses the rendering of another block. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-masks-and-mattes) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. A mask is a shape that cuts its block down. A block can have more than one mask, and the masks combine in their order. A track matte masks a block with the rendered alpha or luminance of another block. Masks and mattes render on the canvas and in raster and PDF export. The editor has no mask or matte controls, so you set them with the block API. The [Lottie importer](https://img.ly/docs/cesdk/mac-catalyst/import-media/lottie-6bb9ab/) on web and Node.js also creates masks and mattes from Lottie files. This guide covers masks, the mask properties and modes, the mask stack, track mattes, the matte source rules and the export limits. ## Creating a Mask Graphic, text and group blocks accept masks. `supportsMasks` returns `true` for them. `createMask` creates a mask block. Give it a shape with `setShape` and attach it with `appendMask`. ```swift highlight-masksAndMattes-createMask let purple = try engine.block.createFill(.color) try engine.block.setColor(purple, property: "fill/color/value", color: .rgba(r: 0.5, g: 0.2, b: 0.9, a: 1)) let ringBlock = try addSquare(x: 50, fill: purple) print(try engine.block.supportsMasks(ringBlock)) // true print(try engine.block.supportsMasks(page)) // false // The ellipse fills the frame of the masked block let outerMask = try engine.block.createMask() try engine.block.setShape(outerMask, shape: engine.block.createShape(.ellipse)) try engine.block.appendMask(ringBlock, maskID: outerMask) ``` The engine evaluates the mask shape in the frame of the masked block, so an ellipse shape fills the whole square. Any shape works, for example an ellipse, a star or a vector path. For the shape types, see [Create Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-shapes-64acc0/). A mask without a shape has no effect. ## Mask Properties Each mask has five properties. Set them with `setEnum`, `setFloat` and `setBool` on the mask block. | Property | Type | Default | Effect | | --- | --- | --- | --- | | `mask/mode` | Enum | `"Add"` | How the mask combines with the masks before it | | `mask/opacity` | Float | `1` | The coverage of the mask, from 0 to 1 | | `mask/feather` | Float | `0` | The softness of the edge, in design units | | `mask/expansion` | Float | `0` | Grows the shape with a positive value and shrinks it with a negative value, in design units | | `mask/inverted` | Bool | `false` | Covers the area outside the shape instead of the area inside | We soften the edge of the ellipse and let 80 percent of the block show through it. ```swift highlight-masksAndMattes-maskProperties // Soften the edge by 20 units and let 80% of the block show through try engine.block.setFloat(outerMask, property: "mask/feather", value: 20) try engine.block.setFloat(outerMask, property: "mask/opacity", value: 0.8) ``` You can animate `mask/opacity`, `mask/feather` and `mask/expansion` with [keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/). ## Combining Masks The masks of a block combine in the order of the stack. The `mask/mode` of each mask sets how it combines with the result of the masks before it. | Mode | Result | | --- | --- | | `"Add"` | Adds the shape | | `"Subtract"` | Removes the shape | | `"Intersect"` | Keeps only the area where the shape and the result overlap | | `"Lighten"` | Keeps the higher coverage of the shape and the result | | `"Darken"` | Keeps the lower coverage of the shape and the result | | `"Difference"` | Adds the shape and removes the area where both overlap | | `"None"` | Has no effect | We shrink a second ellipse by 100 units on each side and subtract it from the first ellipse. The result is a ring. ```swift highlight-masksAndMattes-combineMasks // Shrink a second ellipse by 100 units and subtract it to cut a ring let innerMask = try engine.block.createMask() try engine.block.setShape(innerMask, shape: engine.block.createShape(.ellipse)) try engine.block.setFloat(innerMask, property: "mask/expansion", value: -100) try engine.block.setEnum(innerMask, property: "mask/mode", value: "Subtract") try engine.block.appendMask(ringBlock, maskID: innerMask) ``` The engine skips `"None"` masks. When the first mask that applies uses `"Subtract"`, it removes its shape from the whole block. ## Inverting a Mask `mask/inverted` makes a mask cover the area outside its shape. An inverted star mask cuts a star-shaped hole into the block. ```swift highlight-masksAndMattes-invertMask let teal = try engine.block.createFill(.color) try engine.block.setColor(teal, property: "fill/color/value", color: .rgba(r: 0, g: 0.6, b: 0.6, a: 1)) let starBlock = try addSquare(x: 550, fill: teal) // An inverted mask keeps the area outside its shape: a star-shaped hole let starMask = try engine.block.createMask() try engine.block.setShape(starMask, shape: engine.block.createShape(.star)) try engine.block.setBool(starMask, property: "mask/inverted", value: true) try engine.block.appendMask(starBlock, maskID: starMask) ``` ## Managing the Mask Stack `getMasks` returns the masks of a block in their order. `removeMask` removes the mask at an index and does not destroy it. `insertMask` adds a mask at an index from 0 to the number of masks. ```swift highlight-masksAndMattes-manageMasks print(try engine.block.getMasks(ringBlock)) // [outerMask, innerMask] // Removing a mask does not destroy it try engine.block.removeMask(ringBlock, index: 1) print(engine.block.isValid(innerMask)) // true // Insert it again after the outer mask try engine.block.insertMask(ringBlock, maskID: innerMask, index: 1) ``` A mask can belong to more than one block. When you change it, all these blocks change. ## Setting a Track Matte `setMatte` masks a block with the rendering of a source block. The mode selects the channel of the source. | Mode | The block shows where the source is | | --- | --- | | `.alpha` | Opaque. This is the default. | | `.alphaInverted` | Transparent | | `.luma` | Bright | | `.lumaInverted` | Dark or transparent | We use a square with a white to black gradient as the source. With `.luma`, the orange square fades from the top to the bottom. ```swift highlight-masksAndMattes-setMatte let orange = try engine.block.createFill(.color) try engine.block.setColor(orange, property: "fill/color/value", color: .rgba(r: 1, g: 0.5, b: 0, a: 1)) let target = try addSquare(x: 1050, fill: orange) // The matte source: a gradient from white at the top to black at the bottom let gradient = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( gradient, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1, g: 1, b: 1, a: 1), stop: 0), GradientColorStop(color: .rgba(r: 0, g: 0, b: 0, a: 1), stop: 1), ], ) let source = try addSquare(x: 1050, fill: gradient) // The target shows where the source is bright. The source is not drawn. try engine.block.setMatte(target, source: source, mode: .luma) ``` The engine does not draw the matte source on its own. One source can be the matte of more than one block. When the source is a group, all children of the group form the matte. The source keeps its own position, so place it over the block. ## Reading and Removing a Matte `getMatte` returns the source and the mode, or `nil` when the block has no matte. To remove a matte, call `setMatte` with `nil` as the source. The source then draws again. ```swift highlight-masksAndMattes-readMatte let matte = try engine.block.getMatte(target) print(matte == BlockMatte(source: source, mode: .luma)) // true print(try engine.block.getMatte(ringBlock) == nil) // true ``` ## Matte Source Rules `setMatte` checks the source and throws an `EngineError` when a rule is broken. - The block and the source are graphic, text or group blocks. - The source is on the same page as the block. - The source is not the block itself or one of its parents. - The source is not a block that the block or one of its parents masks, and it does not contain such a block. ```swift highlight-masksAndMattes-matteRules do { // A block cannot use itself as its matte source try engine.block.setMatte(target, source: target) } catch let error as EngineError { print(error.catalogCode == .blockMatteSourceInvalid) // true } ``` `insertChild(into:child:at:)` and `appendChild(to:child:)` keep these rules too. A move that puts a matted block inside its source, or the block and its source on different pages, throws `.blockMatteSourceInvalid`. Move the block and its source together, for example as one group. A source can draw nothing, for example when it is hidden or outside its time range. Then `.alpha` or `.luma` mattes hide the block, and `.alphaInverted` or `.lumaInverted` mattes show it whole. ## Exporting We export the page as PNG and as SVG. The PNG shows the soft ring, the star-shaped hole and the faded orange square. ```swift highlight-masksAndMattes-export let png = try await engine.block.export(page, mimeType: .png) // SVG export ignores the mask opacity and feather, and draws the matted block whole let svg = try await engine.block.export(page, mimeType: .svg) print("Exported \(png.count) PNG bytes and \(svg.count) SVG bytes") ``` SVG export has limits: - SVG export ignores `mask/opacity` and `mask/feather`. The mask shapes still cut the block. - SVG export does not apply mattes. A matted block exports whole, and the source is not drawn. - When you export a matted block alone, the export keeps its matte. For more about export formats, see [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/). ## Masks, Mattes and Other Block Operations - **Undo and redo** restore the mask stack, the mask properties and the matte. - **Saving and loading** a scene keeps the masks and the matte with its mode. - **Duplicating** a block copies its masks and their shapes. A duplicate of the matted block alone uses the same source. A duplicate of a group that holds both blocks uses the copied source. - **Destroying** a block destroys its own masks. A mask that another block also uses stays. Destroying a mask removes it from the stack, and destroying the source removes the matte. - **Scaling** a block or **changing the design unit** also converts `mask/feather` and `mask/expansion`. `appendMask`, `insertMask`, `removeMask` and `setMatte` need the `layer/clipping` scope. `setMatte` also needs `layer/visibility` on the source, because the source stops drawing on its own. ## Troubleshooting - **The block does not accept masks** (`.blockMasksUnsupported`): only graphic, text and group blocks have a mask stack. Check `supportsMasks` first. - **The mask is not accepted** (`.blockMaskInvalid`): pass a block that `createMask` made. - **The index is out of range** (`.blockMaskIndexOutOfBounds`): insert at an index from 0 to the number of masks, and remove at an index below it. - **The block cannot have or be a matte** (`.blockMatteUnsupported`): use graphic, text or group blocks on both sides. - **The source is rejected** (`.blockMatteSourceInvalid`): use a source on the same page that follows the matte source rules. - **The block is not visible**: an `.alpha` or `.luma` matte hides the block when its source is hidden or outside its time range. ## API Reference | Method | Description | | --- | --- | | `engine.block.supportsMasks(_:)` | Check whether a block accepts masks | | `engine.block.createMask()` | Create a mask block | | `engine.block.setShape(_:shape:)` | Give the mask its shape | | `engine.block.appendMask(_:maskID:)` | Add a mask at the end of the stack | | `engine.block.insertMask(_:maskID:index:)` | Add a mask at an index | | `engine.block.getMasks(_:)` | Get the masks of a block in their order | | `engine.block.removeMask(_:index:)` | Remove the mask at an index | | `engine.block.setMatte(_:source:mode:)` | Set or remove the matte of a block | | `engine.block.getMatte(_:)` | Get the `BlockMatte` of a block, or `nil` | | `engine.block.export(_:mimeType:)` | Export the page as PNG or SVG | ## Next Steps - [Blend Modes](https://img.ly/docs/cesdk/mac-catalyst/create-composition/blend-modes-ad3519/): control how a block mixes with the blocks below it. - [Keyframes](https://img.ly/docs/cesdk/mac-catalyst/animation/keyframes-267836/): animate the mask properties over time. - [Import Lottie](https://img.ly/docs/cesdk/mac-catalyst/import-media/lottie-6bb9ab/): turn Lottie masks and mattes into blocks. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Multi-Page Layouts" description: "Create and manage multi-page designs in CE.SDK for documents like brochures, presentations, and catalogs with multiple pages in a single scene." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/multi-page-4d2b50/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Multi-Page Layouts](https://img.ly/docs/cesdk/mac-catalyst/create-composition/multi-page-4d2b50/) --- ```swift file=@cesdk_swift_examples/engine-guides-multi-page/MultiPage.swift reference-only import Foundation import IMGLYEngine @MainActor func multiPage(engine: Engine) async throws { // Create a scene with HorizontalStack layout try engine.scene.create(sceneLayout: .horizontalStack) // Get the stack container let stacks = try engine.block.find(byType: .stack) let stack = stacks[0] // Create the first page let firstPage = try engine.block.create(.page) try engine.block.setWidth(firstPage, value: 800) try engine.block.setHeight(firstPage, value: 600) try engine.block.appendChild(to: stack, child: firstPage) // Resolve sample assets against the engine's configured base URL. let baseURL = try engine.guidesBaseURL // Add spacing between pages (20 pixels in screen space) try engine.block.setFloat(stack, property: "stack/spacing", value: 20) try engine.block.setBool(stack, property: "stack/spacingInScreenspace", value: true) // Add content to the first page let imageBlock1 = try engine.block.create(.graphic) let rectShape1 = try engine.block.createShape(.rect) try engine.block.setShape(imageBlock1, shape: rectShape1) try engine.block.setWidth(imageBlock1, value: 300) try engine.block.setHeight(imageBlock1, value: 200) try engine.block.setPositionX(imageBlock1, value: 250) try engine.block.setPositionY(imageBlock1, value: 200) let imageFill1 = try engine.block.createFill(.image) try engine.block.setURL( imageFill1, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock1, fill: imageFill1) try engine.block.appendChild(to: firstPage, child: imageBlock1) // Create a second page with different content let secondPage = try engine.block.create(.page) try engine.block.setWidth(secondPage, value: 800) try engine.block.setHeight(secondPage, value: 600) try engine.block.appendChild(to: stack, child: secondPage) // Add a different image to the second page let imageBlock2 = try engine.block.create(.graphic) let rectShape2 = try engine.block.createShape(.rect) try engine.block.setShape(imageBlock2, shape: rectShape2) try engine.block.setWidth(imageBlock2, value: 300) try engine.block.setHeight(imageBlock2, value: 200) try engine.block.setPositionX(imageBlock2, value: 250) try engine.block.setPositionY(imageBlock2, value: 200) let imageFill2 = try engine.block.createFill(.image) try engine.block.setURL( imageFill2, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) try engine.block.setFill(imageBlock2, fill: imageFill2) try engine.block.appendChild(to: secondPage, child: imageBlock2) try engine.block.select(firstPage) try engine.scene.enableZoomAutoFit(firstPage, axis: .both) } ``` Create multi-page designs in CE.SDK for brochures, presentations, catalogs, and other documents requiring multiple pages within a single scene. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-multi-page) Multi-page layouts allow you to create documents with multiple artboards within a single scene. Each page operates as an independent canvas that can contain different content while sharing the same scene context. CE.SDK provides scene layout modes that automatically arrange pages vertically, horizontally, or in a free-form canvas. This guide covers how to create multi-page scenes, add pages, and configure spacing between pages. ## Using the Built-in Page Management UI The CE.SDK editor provides built-in UI controls for managing pages. Users can interact with the page panel to add new pages, duplicate existing ones, reorder them with drag-and-drop, delete pages, and navigate between pages by tapping. The page panel displays thumbnails of all pages in the scene, making it easy to understand the document structure at a glance. When a page thumbnail is tapped, the viewport automatically zooms to that page. ## Creating Multi-Page Scenes Programmatically We can create scenes with multiple pages using the engine API. The scene acts as a container for pages, and each page can hold any number of content blocks. ### Creating a Scene with Pages We create a new scene using `engine.scene.create(sceneLayout:)` and specify the layout type. The layout type determines how pages are arranged in the viewport. After creating the scene, we look up the stack container with `engine.block.find(byType: .stack)` and append pages to it. ```swift highlight-multiPage-createScene // Create a scene with HorizontalStack layout try engine.scene.create(sceneLayout: .horizontalStack) // Get the stack container let stacks = try engine.block.find(byType: .stack) let stack = stacks[0] // Create the first page let firstPage = try engine.block.create(.page) try engine.block.setWidth(firstPage, value: 800) try engine.block.setHeight(firstPage, value: 600) try engine.block.appendChild(to: stack, child: firstPage) ``` The scene is created with a `.horizontalStack` layout, meaning pages are arranged side by side from left to right. We then create a page, set its dimensions, and append it to the stack container. ### Configuring Page Spacing We can add spacing between pages in a stack layout using the `stack/spacing` property. This creates visual separation between pages. ```swift highlight-multiPage-stackSpacing // Add spacing between pages (20 pixels in screen space) try engine.block.setFloat(stack, property: "stack/spacing", value: 20) try engine.block.setBool(stack, property: "stack/spacingInScreenspace", value: true) ``` Setting `stack/spacingInScreenspace` to `true` means the spacing value is interpreted as screen pixels, maintaining consistent visual spacing regardless of zoom level. The distinction applies to the canvas preview only. Exports always interpret the spacing in design units, whatever the property is set to. Switching the property never converts the number. A spacing of `40` stays `40` and is reinterpreted in the other space, which changes how far apart the pages appear. ### Adding More Pages To add additional pages, we create new page blocks, set their dimensions, and append them to the stack container. ```swift highlight-multiPage-addPage // Create a second page with different content let secondPage = try engine.block.create(.page) try engine.block.setWidth(secondPage, value: 800) try engine.block.setHeight(secondPage, value: 600) try engine.block.appendChild(to: stack, child: secondPage) // Add a different image to the second page let imageBlock2 = try engine.block.create(.graphic) let rectShape2 = try engine.block.createShape(.rect) try engine.block.setShape(imageBlock2, shape: rectShape2) try engine.block.setWidth(imageBlock2, value: 300) try engine.block.setHeight(imageBlock2, value: 200) try engine.block.setPositionX(imageBlock2, value: 250) try engine.block.setPositionY(imageBlock2, value: 200) let imageFill2 = try engine.block.createFill(.image) try engine.block.setURL( imageFill2, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) try engine.block.setFill(imageBlock2, fill: imageFill2) try engine.block.appendChild(to: secondPage, child: imageBlock2) ``` Each page can contain different content. Here we add different images to each page to demonstrate independent page content. ## Scene Layout Types CE.SDK supports different layout modes that control how pages are arranged on the canvas. You specify the layout type when creating the scene with `engine.scene.create(sceneLayout:)`. **Free Layout** (`.free`) is the default where pages can be positioned anywhere on the canvas. This provides complete control over page placement. **VerticalStack Layout** (`.verticalStack`) arranges pages automatically in a vertical stack from top to bottom. This is useful for scroll-based document previews. **HorizontalStack Layout** (`.horizontalStack`) arranges pages side by side from left to right. This is useful for carousel-style presentations or side-by-side comparisons. ## Setting the Zoom Level We can focus the viewport on a specific page using `engine.scene.enableZoomAutoFit(_:axis:)`. This keeps the target page fitted to the viewport as the scene changes. ```swift highlight-multiPage-zoom try engine.block.select(firstPage) try engine.scene.enableZoomAutoFit(firstPage, axis: .both) ``` ## Troubleshooting **Page not visible after creation**: Ensure the page is attached to the stack with `appendChild(to:child:)` and has valid dimensions set with `setWidth(_:value:)` and `setHeight(_:value:)`. **Cannot add content to page**: Verify you're appending blocks to the page block, not the scene directly. Content blocks should be children of pages. **Pages overlapping**: When using stack layouts, make sure pages are appended to the stack container (found via `find(byType: .stack)`), not directly to the scene. **Spacing not visible**: Check that `stack/spacing` is set to a positive value and that you're using a stack layout (`.horizontalStack` or `.verticalStack`). --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Overview" description: "Combine and arrange multiple elements to create complex, multi-page, or layered design compositions." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/overview-5b19c5/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/create-composition/overview-5b19c5/) --- In CreativeEditor SDK (CE.SDK), a *composition* is an arrangement of multiple design elements—such as images, text, shapes, graphics, and effects—combined into a single, cohesive visual layout. Unlike working with isolated elements, compositions allow you to design complex, multi-element visuals that tell a richer story or support more advanced use cases. All composition processing is handled entirely on the client side, ensuring fast, secure, and efficient editing without requiring server infrastructure. You can use compositions to create a wide variety of projects, including social media posts, marketing materials, collages, and multi-page exports like PDFs. Whether you are building layouts manually through the UI or generating them dynamically with code, compositions give you the flexibility and control to design at scale. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## Working with Multiple Pages and Artboards CE.SDK supports working with multiple artboards or canvases within a single document, enabling you to design multi-page layouts or create several design variations within the same project. Typical multi-page use cases include: - Designing multi-page marketing brochures. - Exporting designs as multi-page PDFs. - Building multiple versions of a design for different audiences or platforms. ## Working with Elements You can easily arrange and manage elements within a composition: - **Positioning and Aligning:** Move elements precisely and align them to each other or to the canvas. - **Guides and Snapping:** Use visual guides and automatic snapping to align objects accurately. - **Grouping:** Group elements for easier collective movement and editing. - **Layer Management:** Control the stacking order and organize elements in layers. - **Locking:** Lock elements to prevent accidental changes during editing. ## UI vs. Programmatic Creation ### Using the UI The CE.SDK UI provides drag-and-drop editing, alignment tools, a layer panel, and snapping guides, making it easy to visually build complex compositions. You can group, align, lock, and arrange elements directly through intuitive controls. ### Programmatic Creation You can also build compositions programmatically by using CE.SDK’s APIs. This is especially useful for: - Automatically generating large volumes of designs. - Creating data-driven layouts. - Integrating CE.SDK into a larger automated workflow. ## Exporting Compositions CE.SDK compositions can be exported in several formats: | Category | Supported Formats | | ----------- | ------------------------------------------------------------------------------------ | | **Images** | `.png` (with transparency), `.jpeg`, `.webp`, `.tga` | | **Vector** | `.svg` (scalable vector graphics with text as paths) | | **Print** | `.pdf` (supports underlayer printing and spot colors) | | **Video** | `.mp4` (H.264 or H.265 on supported platforms with limited transparency support), `.webm` (VP8, VP9 or AV1 with Opus or Vorbis audio; VP8 and VP9 keep transparency on Android, iOS and macOS) | | **Scene** | `.imgly` or `.scene` (description of the scene without any assets) | | **Archive** | `.imgly` or `.zip` (fully self-contained archive that bundles the scene file with all assets) | > **Note:** Our custom cross-platform C++ based rendering and layout engine ensures > consistent output quality across devices. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Positioning and Alignment" description: "Precisely position, align, and distribute objects using guides, snapping, and alignment tools." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/position-and-align-cc6b6a/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Position and Align](https://img.ly/docs/cesdk/mac-catalyst/create-composition/position-and-align-cc6b6a/) --- Position, align, and distribute design elements precisely using CE.SDK's layout APIs and snapping system. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-position-and-align) CE.SDK positions blocks relative to their parent container with the origin at the top left. You can set positions using absolute values (design units) or as percentages of the parent's dimensions. For multi-element layouts, alignment and distribution APIs arrange blocks precisely without manual calculations. Snapping settings let you tune the visual guides shown when users drag elements in the editor. ```swift file=@cesdk_swift_examples/engine-guides-position-and-align/PositionAndAlign.swift reference-only import Foundation import IMGLYEngine @MainActor func positionAndAlign(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Resolve sample assets against the bundled asset base URL. let baseURL = try engine.guidesBaseURL let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") // Block 1: Absolute positioning at specific coordinates (in design units). let block1 = try engine.block.create(.graphic) try engine.block.setShape(block1, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block1, value: 150) try engine.block.setHeight(block1, value: 100) let fill1 = try engine.block.createFill(.image) try engine.block.setURL(fill1, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(block1, fill: fill1) try engine.block.appendChild(to: page, child: block1) try engine.block.setPositionX(block1, value: 50) try engine.block.setPositionY(block1, value: 50) // Query the current position. let x1 = try engine.block.getPositionX(block1) let y1 = try engine.block.getPositionY(block1) print("Block 1 position: (\(x1), \(y1))") // Block 2: Percentage-based positioning relative to the parent's size. let block2 = try engine.block.create(.graphic) try engine.block.setShape(block2, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block2, value: 150) try engine.block.setHeight(block2, value: 100) let fill2 = try engine.block.createFill(.image) try engine.block.setURL(fill2, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(block2, fill: fill2) try engine.block.appendChild(to: page, child: block2) // Switch position modes to .percent and use fractional values where // 1.0 represents 100% of the parent's size. try engine.block.setPositionXMode(block2, mode: .percent) try engine.block.setPositionYMode(block2, mode: .percent) try engine.block.setPositionX(block2, value: 0.5) // 50% from left try engine.block.setPositionY(block2, value: 0.5) // 50% from top // Query the position mode. let xMode = try engine.block.getPositionXMode(block2) let yMode = try engine.block.getPositionYMode(block2) print("Block 2 position modes: X=\(xMode), Y=\(yMode)") // Build a small set of blocks for alignment. var alignBlocks: [DesignBlockID] = [] let alignPositions: [(Float, Float)] = [(100, 100), (250, 150), (180, 250), (350, 200)] for (x, y) in alignPositions { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 100) try engine.block.setHeight(block, value: 80) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(block, fill: fill) try engine.block.appendChild(to: page, child: block) try engine.block.setPositionX(block, value: x) try engine.block.setPositionY(block, value: y) alignBlocks.append(block) } // Confirm the blocks support alignment before calling alignment APIs. let canAlign = try engine.block.isAlignable(alignBlocks) print("Can align blocks: \(canAlign)") // Align the blocks to the left edge of their combined bounding box. if canAlign { try engine.block.alignHorizontally(alignBlocks, alignment: .left) } // Passing a single block aligns it to its parent rather than to a group // bounding box. This is convenient for centering an element on a page. let singleBlock = try engine.block.create(.graphic) try engine.block.setShape(singleBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(singleBlock, value: 150) try engine.block.setHeight(singleBlock, value: 100) let singleFill = try engine.block.createFill(.image) try engine.block.setURL(singleFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(singleBlock, fill: singleFill) try engine.block.appendChild(to: page, child: singleBlock) try engine.block.setPositionX(singleBlock, value: 500) try engine.block.setPositionY(singleBlock, value: 300) if try engine.block.isAlignable([singleBlock]) { try engine.block.alignHorizontally([singleBlock], alignment: .center) try engine.block.alignVertically([singleBlock], alignment: .center) } // Build another row of blocks at uneven horizontal positions for distribution. var distributeBlocks: [DesignBlockID] = [] let xPositions: [Float] = [50, 180, 400, 650] for x in xPositions { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 100) try engine.block.setHeight(block, value: 80) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(block, fill: fill) try engine.block.appendChild(to: page, child: block) try engine.block.setPositionX(block, value: x) try engine.block.setPositionY(block, value: 200) distributeBlocks.append(block) } // Confirm the blocks support distribution before calling distribution APIs. let canDistribute = try engine.block.isDistributable(distributeBlocks) print("Can distribute blocks: \(canDistribute)") // Distribute blocks horizontally so the space between them is even. // The first and last blocks remain in place. if canDistribute { try engine.block.distributeHorizontally(distributeBlocks) } // Build a column of blocks at uneven vertical positions for vertical // distribution. var verticalBlocks: [DesignBlockID] = [] let yPositions: [Float] = [50, 150, 350, 500] for y in yPositions { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 100) try engine.block.setHeight(block, value: 80) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(block, fill: fill) try engine.block.appendChild(to: page, child: block) try engine.block.setPositionX(block, value: 600) try engine.block.setPositionY(block, value: y) verticalBlocks.append(block) } if try engine.block.isDistributable(verticalBlocks) { try engine.block.distributeVertically(verticalBlocks) } // Configure the position snapping threshold (in pixels). Higher values // make snapping activate from further away. try engine.editor.setSettingFloat("positionSnappingThreshold", value: 10) // Configure the rotation snapping threshold (in radians). try engine.editor.setSettingFloat("rotationSnappingThreshold", value: 5 * .pi / 180) // Customize snapping guide colors. `snappingGuideColor` controls the // position snapping lines and `rotationSnappingGuideColor` controls the // rotation guides. try engine.editor.setSettingColor( "snappingGuideColor", color: .rgba(r: 0.2, g: 0.6, b: 1.0, a: 1.0), ) try engine.editor.setSettingColor( "rotationSnappingGuideColor", color: .rgba(r: 1.0, g: 0.4, b: 0.2, a: 1.0), ) } ``` This guide covers how to set block positions using different modes, align blocks horizontally and vertically, distribute blocks with even spacing, and configure snapping for interactive editing. ## Setup We start with a scene and a page that we will use as the parent for every block created in this guide. ```swift highlight-positionAndAlign-setup let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) ``` ## Coordinate System CE.SDK uses a coordinate system where the origin (0, 0) is at the top-left corner of the parent container. The X axis extends to the right and the Y axis extends downward. All positions are relative to the block's parent. ## Setting Block Positions ### Absolute Positioning We can position blocks using absolute coordinates in design units. This is useful when you need precise control over element placement. ```swift highlight-positionAndAlign-absolutePosition // Block 1: Absolute positioning at specific coordinates (in design units). let block1 = try engine.block.create(.graphic) try engine.block.setShape(block1, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block1, value: 150) try engine.block.setHeight(block1, value: 100) let fill1 = try engine.block.createFill(.image) try engine.block.setURL(fill1, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(block1, fill: fill1) try engine.block.appendChild(to: page, child: block1) try engine.block.setPositionX(block1, value: 50) try engine.block.setPositionY(block1, value: 50) // Query the current position. let x1 = try engine.block.getPositionX(block1) let y1 = try engine.block.getPositionY(block1) print("Block 1 position: (\(x1), \(y1))") ``` `setPositionX(_:value:)` and `setPositionY(_:value:)` set the block's position relative to its parent. Use `getPositionX(_:)` and `getPositionY(_:)` to query the current position. ### Percentage-Based Positioning Positions can also be set as percentages of the parent's dimensions. This approach is useful for layouts that should adapt to different container sizes. ```swift highlight-positionAndAlign-percentPosition // Block 2: Percentage-based positioning relative to the parent's size. let block2 = try engine.block.create(.graphic) try engine.block.setShape(block2, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block2, value: 150) try engine.block.setHeight(block2, value: 100) let fill2 = try engine.block.createFill(.image) try engine.block.setURL(fill2, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(block2, fill: fill2) try engine.block.appendChild(to: page, child: block2) // Switch position modes to .percent and use fractional values where // 1.0 represents 100% of the parent's size. try engine.block.setPositionXMode(block2, mode: .percent) try engine.block.setPositionYMode(block2, mode: .percent) try engine.block.setPositionX(block2, value: 0.5) // 50% from left try engine.block.setPositionY(block2, value: 0.5) // 50% from top // Query the position mode. let xMode = try engine.block.getPositionXMode(block2) let yMode = try engine.block.getPositionYMode(block2) print("Block 2 position modes: X=\(xMode), Y=\(yMode)") ``` Position modes are set using `setPositionXMode(_:mode:)` and `setPositionYMode(_:mode:)`. When set to `.percent`, position values represent a fraction of the parent's size (`0.5` = 50%). Query the current mode with `getPositionXMode(_:)` and `getPositionYMode(_:)`. The third mode, `.auto`, lets the engine determine the position automatically. ## Aligning Blocks ### Aligning Multiple Blocks Multiple blocks can be aligned within their combined bounding box. This is useful for creating visually organized layouts. ```swift highlight-positionAndAlign-checkAlignable // Build a small set of blocks for alignment. var alignBlocks: [DesignBlockID] = [] let alignPositions: [(Float, Float)] = [(100, 100), (250, 150), (180, 250), (350, 200)] for (x, y) in alignPositions { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 100) try engine.block.setHeight(block, value: 80) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(block, fill: fill) try engine.block.appendChild(to: page, child: block) try engine.block.setPositionX(block, value: x) try engine.block.setPositionY(block, value: y) alignBlocks.append(block) } // Confirm the blocks support alignment before calling alignment APIs. let canAlign = try engine.block.isAlignable(alignBlocks) print("Can align blocks: \(canAlign)") ``` Before aligning, we check if the blocks can be aligned using `isAlignable(_:)`. This method returns `true` if the blocks support alignment operations. ```swift highlight-positionAndAlign-alignHorizontal // Align the blocks to the left edge of their combined bounding box. if canAlign { try engine.block.alignHorizontally(alignBlocks, alignment: .left) } ``` `alignHorizontally(_:alignment:)` accepts an array of block IDs and a `HorizontalBlockAlignment` value: `.left`, `.right`, or `.center`. Similarly, `alignVertically(_:alignment:)` accepts `VerticalBlockAlignment` values `.top`, `.bottom`, or `.center`. ### Aligning a Single Block to Parent When you pass a single block to the alignment methods, it aligns within its parent container rather than a group bounding box. ```swift highlight-positionAndAlign-alignSingleBlock // Passing a single block aligns it to its parent rather than to a group // bounding box. This is convenient for centering an element on a page. let singleBlock = try engine.block.create(.graphic) try engine.block.setShape(singleBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(singleBlock, value: 150) try engine.block.setHeight(singleBlock, value: 100) let singleFill = try engine.block.createFill(.image) try engine.block.setURL(singleFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(singleBlock, fill: singleFill) try engine.block.appendChild(to: page, child: singleBlock) try engine.block.setPositionX(singleBlock, value: 500) try engine.block.setPositionY(singleBlock, value: 300) if try engine.block.isAlignable([singleBlock]) { try engine.block.alignHorizontally([singleBlock], alignment: .center) try engine.block.alignVertically([singleBlock], alignment: .center) } ``` This approach is useful for centering elements on a page or positioning them at specific edges of the container. ## Distributing Blocks Distribution spaces blocks evenly within their bounding box. This is ideal for creating consistent spacing in grid layouts or navigation elements. ```swift highlight-positionAndAlign-checkDistributable // Build another row of blocks at uneven horizontal positions for distribution. var distributeBlocks: [DesignBlockID] = [] let xPositions: [Float] = [50, 180, 400, 650] for x in xPositions { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 100) try engine.block.setHeight(block, value: 80) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(block, fill: fill) try engine.block.appendChild(to: page, child: block) try engine.block.setPositionX(block, value: x) try engine.block.setPositionY(block, value: 200) distributeBlocks.append(block) } // Confirm the blocks support distribution before calling distribution APIs. let canDistribute = try engine.block.isDistributable(distributeBlocks) print("Can distribute blocks: \(canDistribute)") ``` `isDistributable(_:)` verifies that the blocks can be distributed. ```swift highlight-positionAndAlign-distributeHorizontal // Distribute blocks horizontally so the space between them is even. // The first and last blocks remain in place. if canDistribute { try engine.block.distributeHorizontally(distributeBlocks) } ``` `distributeHorizontally(_:)` arranges blocks so the horizontal space between them is equal. The first and last blocks remain in place while the middle blocks are repositioned. ```swift highlight-positionAndAlign-distributeVertical // Build a column of blocks at uneven vertical positions for vertical // distribution. var verticalBlocks: [DesignBlockID] = [] let yPositions: [Float] = [50, 150, 350, 500] for y in yPositions { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 100) try engine.block.setHeight(block, value: 80) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(block, fill: fill) try engine.block.appendChild(to: page, child: block) try engine.block.setPositionX(block, value: 600) try engine.block.setPositionY(block, value: y) verticalBlocks.append(block) } if try engine.block.isDistributable(verticalBlocks) { try engine.block.distributeVertically(verticalBlocks) } ``` Similarly, `distributeVertically(_:)` distributes blocks with equal vertical spacing. ## Configuring Snapping Snapping provides visual guides when dragging elements in the editor, helping users align blocks precisely. Configure the snapping sensitivity and appearance using editor settings. ### Setting Snapping Thresholds ```swift highlight-positionAndAlign-snappingThreshold // Configure the position snapping threshold (in pixels). Higher values // make snapping activate from further away. try engine.editor.setSettingFloat("positionSnappingThreshold", value: 10) // Configure the rotation snapping threshold (in radians). try engine.editor.setSettingFloat("rotationSnappingThreshold", value: 5 * .pi / 180) ``` The `positionSnappingThreshold` setting controls how close (in pixels) a block must be to a snap target before snapping activates. Higher values make snapping more "sticky". The `rotationSnappingThreshold` setting controls rotation snapping sensitivity in radians. ### Customizing Snapping Guide Colors ```swift highlight-positionAndAlign-snappingColors // Customize snapping guide colors. `snappingGuideColor` controls the // position snapping lines and `rotationSnappingGuideColor` controls the // rotation guides. try engine.editor.setSettingColor( "snappingGuideColor", color: .rgba(r: 0.2, g: 0.6, b: 1.0, a: 1.0), ) try engine.editor.setSettingColor( "rotationSnappingGuideColor", color: .rgba(r: 1.0, g: 0.4, b: 0.2, a: 1.0), ) ``` Customize the appearance of snapping guides using color settings. `snappingGuideColor` controls position snapping lines and `rotationSnappingGuideColor` controls rotation guides. ## Troubleshooting ### Position Not Updating If a block's position doesn't change after calling the setter methods: - Verify the block's transform is not locked with `isTransformLocked(_:)` - Check that the block has the `"layer/move"` scope enabled - Ensure you're using the correct position mode for your values ### Alignment Not Working If `alignHorizontally(_:alignment:)` or `alignVertically(_:alignment:)` has no effect: - Confirm `isAlignable(_:)` returns `true` for the blocks - Verify all block IDs in the array are valid - Check that blocks have the `"layer/move"` scope enabled - Check that no block is transform-locked, either on its own or by a group or frame above it ### Blocks Cannot Be Distributed If `distributeHorizontally(_:)` or `distributeVertically(_:)` doesn't work: - Verify `isDistributable(_:)` returns `true` - Ensure you have at least three blocks in the array - Check that all blocks share the same parent - Check that no block is transform-locked, either on its own or by a group or frame above it ## API Reference | Method | Description | |--------|-------------| | `block.getPositionX(_:)` | Get a block's X position | | `block.getPositionY(_:)` | Get a block's Y position | | `block.setPositionX(_:value:)` | Set a block's X position | | `block.setPositionY(_:value:)` | Set a block's Y position | | `block.getPositionXMode(_:)` | Get a block's X position mode | | `block.getPositionYMode(_:)` | Get a block's Y position mode | | `block.setPositionXMode(_:mode:)` | Set a block's X position mode | | `block.setPositionYMode(_:mode:)` | Set a block's Y position mode | | `block.isAlignable(_:)` | Check if blocks can be aligned | | `block.alignHorizontally(_:alignment:)` | Align blocks horizontally | | `block.alignVertically(_:alignment:)` | Align blocks vertically | | `block.isDistributable(_:)` | Check if blocks can be distributed | | `block.distributeHorizontally(_:)` | Distribute blocks horizontally with even spacing | | `block.distributeVertically(_:)` | Distribute blocks vertically with even spacing | | `editor.setSettingFloat(_:value:)` | Set a float setting (snapping thresholds) | | `editor.setSettingColor(_:color:)` | Set a color setting (snapping guide colors) | ## Next Steps Now that you understand positioning and alignment, explore related layout features: - [Layer Management](https://img.ly/docs/cesdk/mac-catalyst/create-composition/layer-management-18f07a/) — Control the stacking order of elements - [Grouping](https://img.ly/docs/cesdk/mac-catalyst/create-composition/group-and-ungroup-62565a/) — Group related elements together - [Multi-Page Layouts](https://img.ly/docs/cesdk/mac-catalyst/create-composition/multi-page-4d2b50/) — Create multi-page designs with multiple pages in a single scene --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Programmatic Creation" description: "Build compositions entirely through code with the CE.SDK Engine for automation, batch processing, and headless rendering." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-composition/programmatic-a688bf/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) > [Programmatic Creation](https://img.ly/docs/cesdk/mac-catalyst/create-composition/programmatic-a688bf/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-composition-programmatic/CreateCompositionProgrammatic.swift reference-only import Foundation import IMGLYEngine @MainActor func createCompositionProgrammatic(engine: Engine) async throws { // Resolve sample assets against the configured base URL (engine `basePath`, // or the product CDN as a fallback). let baseURL = try engine.guidesBaseURL // Roboto typeface with all variants for mixed styling let robotoBase = baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto") let robotoTypeface = Typeface( name: "Roboto", fonts: [ Font( uri: robotoBase.appendingPathComponent("Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), Font( uri: robotoBase.appendingPathComponent("Roboto-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ), Font( uri: robotoBase.appendingPathComponent("Roboto-Italic.ttf"), subFamily: "Italic", weight: .normal, style: .italic, ), Font( uri: robotoBase.appendingPathComponent("Roboto-BoldItalic.ttf"), subFamily: "Bold Italic", weight: .bold, style: .italic, ), ], ) // Create a scene and a page with social media dimensions (1080x1080) let scene = try engine.scene.create() try engine.block.setFloat(scene, property: "scene/dpi", value: 300) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1080) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) // Set page background to a light lavender color let backgroundFill = try engine.block.createFill(.color) try engine.block.setColor( backgroundFill, property: "fill/color/value", color: .rgba(r: 0.94, g: 0.93, b: 0.98, a: 1.0), ) try engine.block.setFill(page, fill: backgroundFill) // Add the main headline text with the Roboto typeface let headline = try engine.block.create(.text) try engine.block.replaceText(headline, text: "Integrate\nCreative Editing\ninto your App") try engine.block.setFont(headline, fontFileURL: robotoTypeface.fonts[0].uri, typeface: robotoTypeface) try engine.block.setFloat(headline, property: "text/lineHeight", value: 0.78) // Apply bold weight and a black color to the whole headline if try engine.block.canToggleBoldFont(headline) { try engine.block.toggleBoldFont(headline) } try engine.block.setTextColor(headline, color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 1.0)) // Fix the container size and let the font scale automatically try engine.block.setWidthMode(headline, mode: .absolute) try engine.block.setHeightMode(headline, mode: .absolute) try engine.block.setWidth(headline, value: 960) try engine.block.setHeight(headline, value: 300) try engine.block.setBool(headline, property: "text/automaticFontSizeEnabled", value: true) try engine.block.setPositionX(headline, value: 60) try engine.block.setPositionY(headline, value: 80) try engine.block.appendChild(to: page, child: headline) // Add the tagline with mixed per-range styling let tagline = try engine.block.create(.text) let taglineText = "in hours,\nnot months." try engine.block.replaceText(tagline, text: taglineText) try engine.block.setFont(tagline, fontFileURL: robotoTypeface.fonts[0].uri, typeface: robotoTypeface) try engine.block.setFloat(tagline, property: "text/lineHeight", value: 0.78) // Style "in hours," — purple and italic let inHoursRange = taglineText.range(of: "in hours,")! try engine.block.setTextColor(tagline, color: .rgba(r: 0.2, g: 0.2, b: 0.8, a: 1.0), in: inHoursRange) if try engine.block.canToggleItalicFont(tagline, in: inHoursRange) { try engine.block.toggleItalicFont(tagline, in: inHoursRange) } // Style "not months." — black and bold let notMonthsRange = taglineText.range(of: "not months.")! try engine.block.setTextColor(tagline, color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 1.0), in: notMonthsRange) if try engine.block.canToggleBoldFont(tagline, in: notMonthsRange) { try engine.block.toggleBoldFont(tagline, in: notMonthsRange) } try engine.block.setWidthMode(tagline, mode: .absolute) try engine.block.setHeightMode(tagline, mode: .absolute) try engine.block.setWidth(tagline, value: 960) try engine.block.setHeight(tagline, value: 220) try engine.block.setBool(tagline, property: "text/automaticFontSizeEnabled", value: true) try engine.block.setPositionX(tagline, value: 60) try engine.block.setPositionY(tagline, value: 551) try engine.block.appendChild(to: page, child: tagline) // Add the CTA title with an explicit font size let ctaTitle = try engine.block.create(.text) try engine.block.replaceText(ctaTitle, text: "Start a Free Trial") try engine.block.setFont(ctaTitle, fontFileURL: robotoTypeface.fonts[0].uri, typeface: robotoTypeface) try engine.block.setFloat(ctaTitle, property: "text/fontSize", value: 80) try engine.block.setFloat(ctaTitle, property: "text/lineHeight", value: 1.0) if try engine.block.canToggleBoldFont(ctaTitle) { try engine.block.toggleBoldFont(ctaTitle) } try engine.block.setTextColor(ctaTitle, color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 1.0)) try engine.block.setWidthMode(ctaTitle, mode: .absolute) try engine.block.setHeightMode(ctaTitle, mode: .auto) try engine.block.setWidth(ctaTitle, value: 664.6) try engine.block.setPositionX(ctaTitle, value: 64) try engine.block.setPositionY(ctaTitle, value: 952) try engine.block.appendChild(to: page, child: ctaTitle) // Add the website URL let ctaURL = try engine.block.create(.text) try engine.block.replaceText(ctaURL, text: "www.img.ly") try engine.block.setFont(ctaURL, fontFileURL: robotoTypeface.fonts[0].uri, typeface: robotoTypeface) try engine.block.setFloat(ctaURL, property: "text/fontSize", value: 80) try engine.block.setFloat(ctaURL, property: "text/lineHeight", value: 1.0) try engine.block.setTextColor(ctaURL, color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 1.0)) try engine.block.setWidthMode(ctaURL, mode: .absolute) try engine.block.setHeightMode(ctaURL, mode: .auto) try engine.block.setWidth(ctaURL, value: 664.6) try engine.block.setPositionX(ctaURL, value: 64) try engine.block.setPositionY(ctaURL, value: 1006) try engine.block.appendChild(to: page, child: ctaURL) // Add a horizontal divider line let dividerLine = try engine.block.create(.graphic) let lineShape = try engine.block.createShape(.line) try engine.block.setShape(dividerLine, shape: lineShape) let lineFill = try engine.block.createFill(.color) try engine.block.setColor(lineFill, property: "fill/color/value", color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 1.0)) try engine.block.setFill(dividerLine, fill: lineFill) try engine.block.setWidth(dividerLine, value: 418) try engine.block.setHeight(dividerLine, value: 11.3) try engine.block.setPositionX(dividerLine, value: 64) try engine.block.setPositionY(dividerLine, value: 460) try engine.block.appendChild(to: page, child: dividerLine) // Add the IMG.LY logo image let logo = try engine.block.create(.graphic) let logoShape = try engine.block.createShape(.rect) try engine.block.setShape(logo, shape: logoShape) let logoFill = try engine.block.createFill(.image) try engine.block.setURL( logoFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(logo, fill: logoFill) try engine.block.setContentFillMode(logo, mode: .contain) try engine.block.setWidth(logo, value: 200) try engine.block.setHeight(logo, value: 65) try engine.block.setPositionX(logo, value: 820) try engine.block.setPositionY(logo, value: 960) try engine.block.appendChild(to: page, child: logo) // Export the composition as a PNG let options = ExportOptions(targetWidth: 1080, targetHeight: 1080) let blob = try await engine.block.export(page, mimeType: .png, options: options) // Write the exported data to disk let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("composition.png") try blob.write(to: outputURL) } ``` Build compositions entirely through code using the CE.SDK Engine for automation, batch processing, and headless rendering. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-composition-programmatic) CE.SDK provides a complete Engine API for building designs through code. Instead of relying on user interactions through an editor UI, you can create scenes, add blocks like text, images, and shapes, and position them programmatically. This approach enables automation workflows, batch processing, server-side rendering, and integration with custom interfaces. This guide covers how to create a scene structure with social media dimensions, set background colors, add text with mixed styling, line shapes, images, and export the finished composition. ## Initialize the Engine We start by declaring a Roboto typeface that includes all variants needed for mixed styling. Setting up the typeface up front means later code can toggle bold or italic without reconfiguring fonts. ```swift highlight-setup // Roboto typeface with all variants for mixed styling let robotoBase = baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto") let robotoTypeface = Typeface( name: "Roboto", fonts: [ Font( uri: robotoBase.appendingPathComponent("Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), Font( uri: robotoBase.appendingPathComponent("Roboto-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ), Font( uri: robotoBase.appendingPathComponent("Roboto-Italic.ttf"), subFamily: "Italic", weight: .normal, style: .italic, ), Font( uri: robotoBase.appendingPathComponent("Roboto-BoldItalic.ttf"), subFamily: "Bold Italic", weight: .bold, style: .italic, ), ], ) ``` The Engine is created once by the caller (for example `try Engine(license: "")`) and passed to the function that builds the composition. ## Create Scene Structure We create the foundation of the composition with social media dimensions (1080x1080 pixels for Instagram). A scene contains one or more pages, and pages contain the design blocks. ```swift highlight-create-scene // Create a scene and a page with social media dimensions (1080x1080) let scene = try engine.scene.create() try engine.block.setFloat(scene, property: "scene/dpi", value: 300) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1080) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) ``` `engine.scene.create()` returns a scene handle. Create a page with `engine.block.create(.page)`, set its dimensions with `setWidth()` and `setHeight()`, then attach it to the scene with `appendChild(to:child:)`. ## Set Page Background We set the page background using a color fill. This demonstrates how to create and assign fills to blocks. ```swift highlight-add-background // Set page background to a light lavender color let backgroundFill = try engine.block.createFill(.color) try engine.block.setColor( backgroundFill, property: "fill/color/value", color: .rgba(r: 0.94, g: 0.93, b: 0.98, a: 1.0), ) try engine.block.setFill(page, fill: backgroundFill) ``` We create a color fill using `createFill(.color)`, set the color via `setColor(_:property:color:)` with the `fill/color/value` property, then assign the fill to the page. ## Add Text Blocks Text blocks allow you to add and style text content. We demonstrate three different approaches to text sizing and styling. ### Create Text and Set Content Create a text block, set its content with `replaceText()`, then bind the Roboto typeface we declared earlier: ```swift highlight-text-create // Add the main headline text with the Roboto typeface let headline = try engine.block.create(.text) try engine.block.replaceText(headline, text: "Integrate\nCreative Editing\ninto your App") try engine.block.setFont(headline, fontFileURL: robotoTypeface.fonts[0].uri, typeface: robotoTypeface) try engine.block.setFloat(headline, property: "text/lineHeight", value: 0.78) ``` ### Style Entire Text Block Apply styling to the entire text block using `toggleBoldFont()` and `setTextColor()`: ```swift highlight-text-style-block // Apply bold weight and a black color to the whole headline if try engine.block.canToggleBoldFont(headline) { try engine.block.toggleBoldFont(headline) } try engine.block.setTextColor(headline, color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 1.0)) ``` ### Enable Automatic Font Sizing Configure the text block to automatically scale its font size to fit within fixed dimensions: ```swift highlight-text-auto-size // Fix the container size and let the font scale automatically try engine.block.setWidthMode(headline, mode: .absolute) try engine.block.setHeightMode(headline, mode: .absolute) try engine.block.setWidth(headline, value: 960) try engine.block.setHeight(headline, value: 300) try engine.block.setBool(headline, property: "text/automaticFontSizeEnabled", value: true) ``` ### Range-based Text Styling Apply different styles to specific character ranges within a single text block: ```swift highlight-text-range-style // Style "in hours," — purple and italic let inHoursRange = taglineText.range(of: "in hours,")! try engine.block.setTextColor(tagline, color: .rgba(r: 0.2, g: 0.2, b: 0.8, a: 1.0), in: inHoursRange) if try engine.block.canToggleItalicFont(tagline, in: inHoursRange) { try engine.block.toggleItalicFont(tagline, in: inHoursRange) } // Style "not months." — black and bold let notMonthsRange = taglineText.range(of: "not months.")! try engine.block.setTextColor(tagline, color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 1.0), in: notMonthsRange) if try engine.block.canToggleBoldFont(tagline, in: notMonthsRange) { try engine.block.toggleBoldFont(tagline, in: notMonthsRange) } ``` Swift's range-based overloads take a `Range` rather than integer offsets, so you can use idiomatic `String.range(of:)` to locate the subrange. Passing `nil` (or omitting the `in:` parameter) targets the entire string. - `setTextColor(_:color:in:)` — apply color to a specific character range - `canToggleBoldFont(_:in:)` / `toggleBoldFont(_:in:)` — toggle bold styling for a range - `canToggleItalicFont(_:in:)` / `toggleItalicFont(_:in:)` — toggle italic styling for a range ### Fixed Font Size Set an explicit font size instead of using automatic sizing: ```swift highlight-text-fixed-size // Add the CTA title with an explicit font size let ctaTitle = try engine.block.create(.text) try engine.block.replaceText(ctaTitle, text: "Start a Free Trial") try engine.block.setFont(ctaTitle, fontFileURL: robotoTypeface.fonts[0].uri, typeface: robotoTypeface) try engine.block.setFloat(ctaTitle, property: "text/fontSize", value: 80) try engine.block.setFloat(ctaTitle, property: "text/lineHeight", value: 1.0) ``` ## Add Shapes We create shapes using graphic blocks. CE.SDK supports `rect`, `line`, `ellipse`, `polygon`, `star`, and `vectorPath` shapes through the `ShapeType` enum. ### Create a Shape Block Create a graphic block and assign a shape to it: ```swift highlight-shape-create // Add a horizontal divider line let dividerLine = try engine.block.create(.graphic) let lineShape = try engine.block.createShape(.line) try engine.block.setShape(dividerLine, shape: lineShape) ``` ### Apply Fill to Shape Create a color fill and apply it to the shape: ```swift highlight-shape-fill let lineFill = try engine.block.createFill(.color) try engine.block.setColor(lineFill, property: "fill/color/value", color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 1.0)) try engine.block.setFill(dividerLine, fill: lineFill) ``` ## Add Images We add images using graphic blocks with image fills. ### Create an Image Block Create a graphic block with a rect shape and an image fill: ```swift highlight-image-create // Add the IMG.LY logo image let logo = try engine.block.create(.graphic) let logoShape = try engine.block.createShape(.rect) try engine.block.setShape(logo, shape: logoShape) let logoFill = try engine.block.createFill(.image) try engine.block.setURL( logoFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(logo, fill: logoFill) ``` We set the image URL via `setString(_:property:value:)` with the `fill/image/imageFileURI` property. ## Position and Size Blocks All blocks use the same positioning and sizing APIs: ```swift highlight-block-position try engine.block.setContentFillMode(logo, mode: .contain) try engine.block.setWidth(logo, value: 200) try engine.block.setHeight(logo, value: 65) try engine.block.setPositionX(logo, value: 820) try engine.block.setPositionY(logo, value: 960) try engine.block.appendChild(to: page, child: logo) ``` - `setWidth(_:value:)` / `setHeight(_:value:)` — set block dimensions - `setPositionX(_:value:)` / `setPositionY(_:value:)` — set block position - `setContentFillMode(_:mode:)` — control how content fills the block (`.crop`, `.cover`, `.contain`) - `appendChild(to:child:)` — add the block to the page hierarchy ## Export the Composition We export the finished composition using the engine API. ### Export Using the Engine API `engine.block.export(_:mimeType:options:)` returns the rendered bytes as `Data` (aliased as `Blob`): ```swift highlight-export-api // Export the composition as a PNG let options = ExportOptions(targetWidth: 1080, targetHeight: 1080) let blob = try await engine.block.export(page, mimeType: .png, options: options) ``` ### Write to File System Write the returned `Data` to disk using `write(to:)`: ```swift highlight-export-file // Write the exported data to disk let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("composition.png") try blob.write(to: outputURL) ``` ## Troubleshooting - **Blocks not appearing**: Verify that `appendChild(to:child:)` attaches blocks to the page. Blocks must be part of the scene hierarchy to render. - **Text styling not applied**: Verify character ranges are correct for range-based APIs. Swift uses `Range`, which correctly handles multi-byte characters when you locate the subrange with `String.range(of:)`. - **Image stretched**: Use `setContentFillMode(_:mode: .contain)` to maintain the image's aspect ratio. - **Export fails**: Verify that page dimensions are set before export. The export requires valid dimensions. - **Typeface missing variants**: If `canToggleBoldFont(_:)` returns `false`, the configured `Typeface` is missing a bold `Font` entry with the matching style. Check that each weight/style combination has a `Font` in the typeface. ## Next Steps - [Layer Management](https://img.ly/docs/cesdk/mac-catalyst/create-composition/layer-management-18f07a/) — Control block stacking and organization - [Positioning and Alignment](https://img.ly/docs/cesdk/mac-catalyst/create-composition/position-and-align-cc6b6a/) — Precise block placement - [Group and Ungroup](https://img.ly/docs/cesdk/mac-catalyst/create-composition/group-and-ungroup-62565a/) — Group blocks for unified transforms - [Blend Modes](https://img.ly/docs/cesdk/mac-catalyst/create-composition/blend-modes-ad3519/) — Control how blocks interact visually - [Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) — Export options and formats --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create a precompiled XCFramework for offline builds" description: "Compiling CE.SDK Swift packages and other project dependencies to a binary XCFramework to support easy building in airgapped environments." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-prebuilt-xcframework-c67971/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Prebuilt XCFramework](https://img.ly/docs/cesdk/mac-catalyst/create-prebuilt-xcframework-c67971/) --- This guide walks you through compiling the IMGLYUI Swift dependency and its dependencies into a XCFramework usable for Xcode builds without internet access or Swift Package Manager access. ## Requirements To work with the SDK, you'll need: - A Mac running a recent version of [Xcode](https://developer.apple.com/xcode/) - macOS Tahoe (26) or newer, as required by Scipio - Your application project for reference ## Install [Scipio](https://github.com/giginet/Scipio) We will make use of the [Scipio](https://github.com/giginet/Scipio) tool, which automates the process of building XCFrameworks from Swift Package Manager dependencies. You can install Scipio with standard Swift package management tools such as [nest](https://github.com/mtj0928/nest), [Mint](https://github.com/yonaskolb/Mint) or directly from source: ```bash nest install giginet/Scipio scipio --help # Or mint install giginet/Scipio mint run scipio --help # Or git clone https://github.com/giginet/Scipio.git cd Scipio swift run -c release scipio --help ``` ## Prepare a dummy Swift Package Manager project to pull in all the dependencies for precompilation First, create an empty directory somewhere and create a `Package.swift` file inside with the following contents: ```swift // swift-tools-version: 6.3.3 // swift-tools-version: 6.3.3 import PackageDescription // Dummy package to bundle dependencies as a precompiled XCFramework let package = Package( name: "DummyApp", // Match the app target version here platforms: [.iOS(.v16)], products: [ .library(name: "DummyApp", targets: ["DummyApp"]) ], // Custom dependencies can be added here dependencies: [ .package(url: "https://github.com/imgly/IMGLYUI-swift.git", exact: "1.86.2"), // If you use these libraries in your app, make sure to match exact versions here .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", exact: "26.0.0"), .package(url: "https://github.com/onevcat/Kingfisher.git", exact: "8.5.0"), ], targets: [ .target( name: "DummyApp", // Make sure to add any custom packages to the list here too dependencies: [.product(name: "IMGLYUI", package: "IMGLYUI-swift")] ) ] ) ``` You can tweak the dependency lists and versions as needed to precompile all your project dependencies into XCFramework bundles. Then, create an empty source file in `Sources/DummyApp/DummyApp.swift` to match the package definition. Make sure the Swift package builds by running `xcodebuild` in the package directory: ```bash xcodebuild -scheme DummyApp -destination 'platform=iOS Simulator,arch=arm64,OS=26.0,name=iPhone SE (3rd generation)' build ``` ## Compile XCFrameworks for all the dependencies with Scipio Run the following command to create a [mergeable](https://developer.apple.com/documentation/xcode/configuring-your-project-to-use-mergeable-libraries) XCFramework for every dependency of the project: (including transitive dependencies) ```bash scipio prepare --support-simulators --framework-type mergeable --enable-library-evolution --overwrite ``` ## Use the resulting XCFrameworks The resulting frameworks are located in the `XCFrameworks` subdirectory by default, and can be added to Xcode projects as dependencies. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create Templates" description: "Learn how to create, import, and manage reusable templates to streamline design creation in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/create-templates/overview-4ebe30/) - Learn how to create, import, and manage reusable templates to streamline design creation in CE.SDK. - [Create From Scratch](https://img.ly/docs/cesdk/mac-catalyst/create-templates/from-scratch-663cda/) - Build reusable design templates programmatically using CE.SDK's APIs. Create scenes, add text and graphic blocks, configure placeholders and variables, apply editing constraints, and save templates for reuse. - [Import Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates/import-e50084/) - Load and import design templates into CE.SDK from URLs, archives, and serialized strings. - [Dynamic Content](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content-53fad7/) - Use variables and placeholders to inject dynamic data into templates at design or runtime. - [Lock the Template](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) - Restrict editing access to specific elements or properties in a template to enforce design rules. - [Edit or Remove Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates/edit-or-remove-38a8be/) - Add, edit, remove, and update design templates in a local asset source with the CE.SDK engine. - [Add to Template Library](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-to-template-library-8bfbc7/) - Save and organize templates in an asset source for users to browse and apply from the template library. - [Overview](https://img.ly/docs/cesdk/mac-catalyst/use-templates/overview-ae74e1/) - Learn how to browse, apply, and dynamically populate templates in CE.SDK to streamline design workflows. - [Template Library](https://img.ly/docs/cesdk/mac-catalyst/use-templates/library-b3c704/) - Learn how to provide a set of predefined templates in the CreativeEditor SDK. - [Apply a Template](https://img.ly/docs/cesdk/mac-catalyst/use-templates/apply-template-35c73e/) - Apply template scenes to an existing scene with the CE.SDK Engine API for Swift, preserving your page dimensions and design unit. - [Generate From Template](https://img.ly/docs/cesdk/mac-catalyst/use-templates/generate-334e15/) - Generate finished designs from templates with the CE.SDK Engine API for Swift by loading, populating variables, and exporting to images and PDFs. - [Replace Content](https://img.ly/docs/cesdk/mac-catalyst/use-templates/replace-content-4c482b/) - Dynamically replace text, images, and placeholder content within templates using CE.SDK's placeholder and variable systems. - [Use Templates Programmatically](https://img.ly/docs/cesdk/mac-catalyst/use-templates/programmatic-9349f3/) - Work with templates programmatically through CE.SDK's engine APIs to load existing templates, build new templates from scratch, modify template structures, and populate templates with dynamic data for batch processing and automation. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Dynamic Content" description: "Use variables and placeholders to inject dynamic data into templates at design or runtime." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content-53fad7/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content-53fad7/) --- ```swift file=@cesdk_swift_examples/engine-guides-dynamic-content/DynamicContent.swift reference-only import Foundation import IMGLYEngine @MainActor func dynamicContent(engine: Engine) async throws { // Resolve sample assets against the engine's configured base URL. let baseURL = try engine.guidesBaseURL // Demo scaffolding: create an 800×600 pixel page to hold the template content. let scene = try engine.scene.create() try engine.scene.setDesignUnit(.px) try engine.block.setFloat(scene, property: "scene/dpi", value: 72) try engine.block.setFloat(scene, property: "scene/pixelScaleFactor", value: 1) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Set the Adopter role before creating the template's content so the engine // enforces the editing scopes configured below. try engine.editor.setRole("Adopter") // Content area: 480px wide, centered (left margin = 160px) let contentX: Float = 160 let contentWidth: Float = 480 try engine.variable.set(key: "firstName", value: "Jane") try engine.variable.set(key: "lastName", value: "Doe") try engine.variable.set(key: "companyName", value: "IMG.LY") // Create heading with variable tokens let headingText = try engine.block.create(.text) try engine.block.replaceText( headingText, text: "Welcome to {{companyName}}, {{firstName}} {{lastName}}.", ) // Discover all variables in the scene let allVariables = engine.variable.findAll() print("Variables in scene:", allVariables) try engine.block.setWidth(headingText, value: contentWidth) try engine.block.setHeightMode(headingText, mode: .auto) try engine.block.setFloat(headingText, property: "text/fontSize", value: 32) try engine.block.setTextHorizontalAlignment(headingText, alignment: .left) try engine.block.appendChild(to: page, child: headingText) try engine.block.setPositionX(headingText, value: contentX) try engine.block.setPositionY(headingText, value: 200) // Create description with bullet points let descriptionText = try engine.block.create(.text) try engine.block.replaceText( descriptionText, text: "This example demonstrates dynamic templates.\n\n" + "• Text Variables — Personalize content with {{tokens}}\n" + "• Placeholders — Swappable images and media\n" + "• Editing Constraints — Protected brand elements", ) try engine.block.setWidth(descriptionText, value: contentWidth) try engine.block.setHeightMode(descriptionText, mode: .auto) try engine.block.setFloat(descriptionText, property: "text/fontSize", value: 20) try engine.block.setTextHorizontalAlignment(descriptionText, alignment: .left) try engine.block.appendChild(to: page, child: descriptionText) try engine.block.setPositionX(descriptionText, value: contentX) try engine.block.setPositionY(descriptionText, value: 300) try await engine.captureGuide(page, label: "after-text-variables") // Demo scaffolding: create a hero image that the placeholder section below // turns into a swappable drop zone. let heroImage = try engine.block.create(.graphic) try engine.block.setShape(heroImage, shape: engine.block.createShape(.rect)) let heroFill = try engine.block.createFill(.image) try engine.block.setURL( heroFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(heroImage, fill: heroFill) try engine.block.setWidth(heroImage, value: contentWidth) try engine.block.setHeight(heroImage, value: 140) try engine.block.appendChild(to: page, child: heroImage) try engine.block.setPositionX(heroImage, value: contentX) try engine.block.setPositionY(heroImage, value: 40) // Enable placeholder behavior on the image fill let fill = try engine.block.getFill(heroImage) if try engine.block.supportsPlaceholderBehavior(fill) { try engine.block.setPlaceholderBehaviorEnabled(fill, enabled: true) } // Enable user interaction and visual controls on the block try engine.block.setPlaceholderEnabled(heroImage, enabled: true) if try engine.block.supportsPlaceholderControls(heroImage) { try engine.block.setPlaceholderControlsOverlayEnabled(heroImage, enabled: true) try engine.block.setPlaceholderControlsButtonEnabled(heroImage, enabled: true) } // Find all placeholders in the scene let placeholders = engine.block.findAllPlaceholders() print("Placeholders in scene:", placeholders.count) // Demo scaffolding: create a brand image that the constraints section below // protects from user edits. let brandImage = try engine.block.create(.graphic) try engine.block.setShape(brandImage, shape: engine.block.createShape(.rect)) let brandFill = try engine.block.createFill(.image) try engine.block.setURL( brandFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg"), ) try engine.block.setFill(brandImage, fill: brandFill) try engine.block.setWidth(brandImage, value: 100) try engine.block.setHeight(brandImage, value: 25) try engine.block.appendChild(to: page, child: brandImage) try engine.block.setPositionX(brandImage, value: 350) try engine.block.setPositionY(brandImage, value: 540) // Lock the brand image: prevent moving, resizing, and selection try engine.block.setScopeEnabled(brandImage, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(brandImage, key: "layer/resize", enabled: false) try engine.block.setScopeEnabled(brandImage, key: "editor/select", enabled: false) // Verify constraints are applied let canSelect = try engine.block.isScopeEnabled(brandImage, key: "editor/select") let canMove = try engine.block.isScopeEnabled(brandImage, key: "layer/move") print("Brand image - canSelect:", canSelect, "canMove:", canMove) try await engine.captureGuide(page, label: "hero") } ``` Dynamic content transforms static designs into flexible, data-driven templates. CE.SDK provides three complementary capabilities—text variables, placeholders, and editing constraints—that work together to enable personalization while maintaining design integrity. ![Dynamic content example with resolved text variables, a swappable hero image, and a protected brand image.](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-dynamic-content) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. This guide covers how to use dynamic content capabilities in CE.SDK templates. The example creates a social media card with personalized name and company variables, a replaceable hero image, and a protected brand image. ## Dynamic Content Capabilities CE.SDK offers three ways to make templates dynamic: - **Text Variables** — Insert `{{tokens}}` in text that resolve to dynamic values at runtime - **Placeholders** — Mark blocks as drop zones where users can swap images or videos - **Editing Constraints** — Lock specific properties to protect brand elements while allowing controlled changes The example sets the Adopter role with `engine.editor.setRole(_:)` before creating the template's content. Under the Adopter role, the engine enforces editing scopes and defers scope decisions to each block's own settings, so the block-level constraints configured below take effect. ## Text Variables Text variables enable data-driven text personalization. Define variables using `engine.variable.set(key:value:)`, then reference them in text blocks with `{{variableName}}` tokens. ```swift highlight-dynamicContent-textVariables try engine.variable.set(key: "firstName", value: "Jane") try engine.variable.set(key: "lastName", value: "Doe") try engine.variable.set(key: "companyName", value: "IMG.LY") // Create heading with variable tokens let headingText = try engine.block.create(.text) try engine.block.replaceText( headingText, text: "Welcome to {{companyName}}, {{firstName}} {{lastName}}.", ) // Discover all variables in the scene let allVariables = engine.variable.findAll() print("Variables in scene:", allVariables) ``` Variables are defined globally and can be referenced in any text block. The `findAll()` method returns all variable keys in the scene, useful for building dynamic editing interfaces. Read a variable's current value with `engine.variable.get(key:)`. > **Note:** Variable keys are case-sensitive. `{{Name}}` and `{{name}}` are different variables. ## Placeholders Placeholders turn design blocks into drop zones for swappable media. Mark an image block as a placeholder, and users can replace its content while the surrounding design remains fixed. ```swift highlight-dynamicContent-placeholders // Enable placeholder behavior on the image fill let fill = try engine.block.getFill(heroImage) if try engine.block.supportsPlaceholderBehavior(fill) { try engine.block.setPlaceholderBehaviorEnabled(fill, enabled: true) } // Enable user interaction and visual controls on the block try engine.block.setPlaceholderEnabled(heroImage, enabled: true) if try engine.block.supportsPlaceholderControls(heroImage) { try engine.block.setPlaceholderControlsOverlayEnabled(heroImage, enabled: true) try engine.block.setPlaceholderControlsButtonEnabled(heroImage, enabled: true) } // Find all placeholders in the scene let placeholders = engine.block.findAllPlaceholders() print("Placeholders in scene:", placeholders.count) ``` For a graphic block, placeholder behavior is a property of its fill: retrieve the fill with `getFill(_:)`, then query support and enable the behavior on that fill. The interactive placeholder flag and the visual controls apply to the block itself — enable user interaction with `setPlaceholderEnabled(_:enabled:)`, and configure the overlay and replace button separately via `setPlaceholderControlsOverlayEnabled(_:enabled:)` and `setPlaceholderControlsButtonEnabled(_:enabled:)`. ## Editing Constraints Editing constraints protect design integrity by limiting what users can modify. Use scope-based APIs to lock specific properties while keeping others editable. ```swift highlight-dynamicContent-editingConstraints // Lock the brand image: prevent moving, resizing, and selection try engine.block.setScopeEnabled(brandImage, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(brandImage, key: "layer/resize", enabled: false) try engine.block.setScopeEnabled(brandImage, key: "editor/select", enabled: false) // Verify constraints are applied let canSelect = try engine.block.isScopeEnabled(brandImage, key: "editor/select") let canMove = try engine.block.isScopeEnabled(brandImage, key: "layer/move") print("Brand image - canSelect:", canSelect, "canMove:", canMove) ``` The `setScopeEnabled(_:key:enabled:)` method controls individual properties. Setting `"editor/select"` to `false` prevents users from selecting the block entirely, making it completely non-interactive. Combined with `"layer/move"` and `"layer/resize"`, this creates a fully protected element. ## Choosing the Right Capability | Need | Capability | | --- | --- | | Dynamic text content | Text Variables | | Swappable images/videos | Placeholders | | Lock specific properties | Editing Constraints | ## API Reference | Method | Description | | --- | --- | | `engine.editor.setRole(_:)` | Set user role (Creator, Adopter, Viewer, Presenter) | | `engine.variable.findAll()` | Get all variable keys in the scene | | `engine.variable.set(key:value:)` | Create or update a text variable | | `engine.variable.get(key:)` | Read a variable's current value | | `engine.block.getFill(_:)` | Get the fill that carries a graphic block's placeholder behavior | | `engine.block.supportsPlaceholderBehavior(_:)` | Check placeholder support | | `engine.block.setPlaceholderBehaviorEnabled(_:enabled:)` | Enable placeholder behavior | | `engine.block.setPlaceholderEnabled(_:enabled:)` | Enable user interaction | | `engine.block.findAllPlaceholders()` | Find all placeholder blocks | | `engine.block.setScopeEnabled(_:key:enabled:)` | Enable or disable editing scope | | `engine.block.isScopeEnabled(_:key:)` | Query scope state | ## Next Steps Each capability has a dedicated deep-dive guide: - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) - Personalize text with tokens resolved at runtime - [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Mark image, video, or text blocks as swappable drop zones - [Set Editing Constraints](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) - Lock specific properties with scope-based permissions - [Form-Based Editing](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/form-based-editing-a8a779/) - Build custom form interfaces to drive template customization --- ## Related Pages - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) - Define dynamic text elements that can be populated with custom values at runtime. - [Variable Bindings](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/variable-bindings-faa12f/) - Drive block colors, sizes, images and typefaces from typed variables, so one change updates every bound property. - [Value Sets](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/value-sets-fa0904/) - Keep named alternative values for scene variables in value sets, and switch the values that the scene shows. - [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Use placeholders to mark editable image, video, or text areas within a locked template layout. - [Set Editing Constraints](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) - Control editing capabilities in CE.SDK templates with the Scope system to lock positions, prevent transformations, and build guided editing experiences in Swift. - [Form-Based Editing](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/form-based-editing-a8a779/) - Build custom form interfaces for template customization using CE.SDK variables and placeholders. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Form-Based Editing" description: "Build custom form interfaces for template customization using CE.SDK variables and placeholders." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/form-based-editing-a8a779/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content-53fad7/) > [Form-Based Editing](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/form-based-editing-a8a779/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-templates-form-based-editing/FormBasedEditing.swift reference-only import Foundation import IMGLYEngine @MainActor func formBasedEditing(engine: Engine) async throws { // Resolve sample images against the engine's configured base URL. let baseURL = try engine.guidesBaseURL // Demo scaffolding: build a small template inline so this example runs // standalone. In production, replace everything up to the "Discover" section // with a single `engine.scene.load(from: templateURL)` call that loads a // template your team authored on the web — its variable tokens, defined // variables, and placeholder blocks are already in place. // Create the scene with a pixel design unit. Passing the design unit to // `create` also pairs the font-size unit to pixels, so the `text/fontSize` // values below are interpreted as pixels — the default font-size unit is // points, which the scene's DPI would otherwise scale up. let scene = try engine.scene.create(designUnit: .px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 600) try engine.block.setHeight(page, value: 800) try engine.block.appendChild(to: scene, child: page) // A heading that references the `tag` variable. It renders with the engine's // default font and a black fill; the example sets a larger font size than the // subtitle for visual hierarchy. let title = try engine.block.create(.text) try engine.block.replaceText(title, text: "{{tag}}!") try engine.block.setFloat(title, property: "text/fontSize", value: 56) try engine.block.setWidth(title, value: 500) try engine.block.setPositionX(title, value: 50) try engine.block.setPositionY(title, value: 50) try engine.block.appendChild(to: page, child: title) let subtitle = try engine.block.create(.text) // Reference a variable in text by wrapping its name in double curly braces. // The engine substitutes `{{tagline}}` with the variable's value at render time. try engine.block.replaceText(subtitle, text: "{{tagline}}") // `referencesAnyVariables(_:)` confirms a block depends on variable tokens. let subtitleUsesVariables = try engine.block.referencesAnyVariables(subtitle) print("Subtitle references variables:", subtitleUsesVariables) // A smaller font size than the heading gives the form a clear hierarchy. try engine.block.setFloat(subtitle, property: "text/fontSize", value: 32) try engine.block.setWidth(subtitle, value: 500) try engine.block.setPositionX(subtitle, value: 50) try engine.block.setPositionY(subtitle, value: 140) try engine.block.appendChild(to: page, child: subtitle) // An image block marked as a placeholder so users can swap its content. let image = try engine.block.create(.graphic) try engine.block.setShape(image, shape: engine.block.createShape(.rect)) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(image, fill: imageFill) try engine.block.setWidth(image, value: 500) try engine.block.setHeight(image, value: 400) try engine.block.setPositionX(image, value: 50) try engine.block.setPositionY(image, value: 250) try engine.block.setPlaceholderEnabled(image, enabled: true) try engine.block.appendChild(to: page, child: image) // Give each variable an initial value. A web-authored template ships with // these defaults already set; here we define them so the form has something // to show on first load. try engine.variable.set(key: "tag", value: "Welcome") try engine.variable.set(key: "tagline", value: "Your personalized design") // List every variable the template defines — render one form field per entry. let variableNames = engine.variable.findAll() // Find image placeholders: graphic blocks flagged as placeholders. let graphicBlocks = try engine.block.find(byType: .graphic) let placeholders = try graphicBlocks.filter { try engine.block.isPlaceholderEnabled($0) } print("Variables:", variableNames, "Placeholders:", placeholders.count) // Read a variable to seed a form field, then write the user's edit back. // In SwiftUI, call the setter from a TextField's `onChange(of:)` handler. let currentTag = try engine.variable.get(key: "tag") print("Seeding field with:", currentTag) try engine.variable.set(key: "tag", value: "Hello") // Read a placeholder's current image so the form can preview it. guard let placeholder = placeholders.first else { return } let fill = try engine.block.getFill(placeholder) let currentImageURL = try engine.block.getURL(fill, property: "fill/image/imageFileURI") print("Current placeholder image:", currentImageURL.lastPathComponent) // Swap the placeholder's content when the user picks a new image. Point the // fill at any local file URL — a photo from the picker, a bundled asset, or // a downloaded file. try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) // Apply an entire form's current values in one pass — for example when the // user taps "Apply". Keep your form state in a dictionary keyed by variable // name and write each entry back through the engine. let formValues: [String: String] = [ "tag": "Welcome Back", "tagline": "Built from form input", ] for (key, value) in formValues where variableNames.contains(key) { try engine.variable.set(key: key, value: value) } // Before exporting, confirm every variable the form exposes has a value. let missingFields = try variableNames.filter { try engine.variable.get(key: $0).isEmpty } guard missingFields.isEmpty else { print("Cannot export — required fields are empty:", missingFields) return } let exported = try await engine.block.export(page, mimeType: .png) print("Exported personalized template:", exported.count, "bytes") try await engine.captureGuide(page, label: "hero", mimeType: .png) } ``` Expose a template's variables and placeholders through your own input controls so users customize designs by filling fields instead of manipulating the canvas — ideal for non-designers and consistent, on-brand output. ![A personalized template rendered by form-based editing — the heading and tagline show variable values substituted into the text, above a swapped placeholder image.](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-templates-form-based-editing) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Form-based editing turns template adoption into structured data entry. Instead of asking users to locate and edit elements on the canvas, you read a template's customization points with the headless Creative Engine and build your own form — text fields for variables, image pickers for placeholders — that writes values back through the engine API. This guide walks through discovering a template's variables and placeholders, reading and updating their values, replacing placeholder images, and validating input before export. ## Understanding Form-Based Editing Form-based editing replaces direct canvas manipulation with input controls you design yourself. Two kinds of customization points drive a form: - **Variables** hold text values referenced from text blocks. Each variable maps to a text field. - **Placeholders** are image blocks marked as editable. Each maps to an image picker. You discover both programmatically, render matching controls in a SwiftUI form, and write user input back through the engine. The engine updates the design immediately, so a live preview reflects every change. Because the form only exposes the fields you choose, the rest of the design stays locked and on-brand. ## Discovering Template Metadata Inspect a loaded template to learn what it lets users customize. `engine.variable.findAll()` returns every defined variable name, and filtering graphic blocks by `isPlaceholderEnabled(_:)` finds the editable images. ```swift highlight-formBasedEditing-discover // List every variable the template defines — render one form field per entry. let variableNames = engine.variable.findAll() // Find image placeholders: graphic blocks flagged as placeholders. let graphicBlocks = try engine.block.find(byType: .graphic) let placeholders = try graphicBlocks.filter { try engine.block.isPlaceholderEnabled($0) } print("Variables:", variableNames, "Placeholders:", placeholders.count) ``` Use one form field per variable name and one image picker per placeholder. `engine.block.findAllPlaceholders()` is a convenience that returns the placeholder blocks directly if you do not need the intermediate graphic-block list. ## Working with Variables Variables store text values that text blocks reference with a `{{variableName}}` token. The engine substitutes the value at render time. ### Using Variables in Text Reference a variable by wrapping its name in double curly braces. `referencesAnyVariables(_:)` confirms whether a block depends on variables before you expose it in a form. ```swift highlight-formBasedEditing-useVariableInText // Reference a variable in text by wrapping its name in double curly braces. // The engine substitutes `{{tagline}}` with the variable's value at render time. try engine.block.replaceText(subtitle, text: "{{tagline}}") // `referencesAnyVariables(_:)` confirms a block depends on variable tokens. let subtitleUsesVariables = try engine.block.referencesAnyVariables(subtitle) print("Subtitle references variables:", subtitleUsesVariables) ``` ### Defining Variables Give each variable an initial value so the form shows something on first load. A template authored on the web ships with these defaults already set; when you build a template in code, define them with `engine.variable.set(key:value:)`. ```swift highlight-formBasedEditing-defineVariables // Give each variable an initial value. A web-authored template ships with // these defaults already set; here we define them so the form has something // to show on first load. try engine.variable.set(key: "tag", value: "Welcome") try engine.variable.set(key: "tagline", value: "Your personalized design") ``` ### Updating Variables Read a variable with `engine.variable.get(key:)` to seed a field, then write the user's edit back with `engine.variable.set(key:value:)`. Call the setter from a `TextField`'s `onChange(of:)` handler so the design updates as the user types. ```swift highlight-formBasedEditing-updateVariables // Read a variable to seed a form field, then write the user's edit back. // In SwiftUI, call the setter from a TextField's `onChange(of:)` handler. let currentTag = try engine.variable.get(key: "tag") print("Seeding field with:", currentTag) try engine.variable.set(key: "tag", value: "Hello") ``` ## Replacing Placeholder Content Placeholders are graphic blocks marked editable, letting users replace images while the rest of the design stays fixed. ### Reading the Current Image Read a placeholder's fill to preview its current image in the form. `engine.block.getURL(_:property:)` returns the image's file URL. ```swift highlight-formBasedEditing-getFill // Read a placeholder's current image so the form can preview it. guard let placeholder = placeholders.first else { return } let fill = try engine.block.getFill(placeholder) let currentImageURL = try engine.block.getURL(fill, property: "fill/image/imageFileURI") print("Current placeholder image:", currentImageURL.lastPathComponent) ``` ### Setting a New Image Swap the image by pointing the same fill at a new file URL with `engine.block.setURL(_:property:value:)`. The URL can come from a photo picker, a bundled asset, or a downloaded file. ```swift highlight-formBasedEditing-setFill // Swap the placeholder's content when the user picks a new image. Point the // fill at any local file URL — a photo from the picker, a bundled asset, or // a downloaded file. try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) ``` ## Driving Updates from Your Own Form Keep your form state in a dictionary keyed by variable name, then write each entry back through the engine in one pass — for example when the user taps "Apply". The same engine API works regardless of the UI framework you build the form with. ```swift highlight-formBasedEditing-driveUpdates // Apply an entire form's current values in one pass — for example when the // user taps "Apply". Keep your form state in a dictionary keyed by variable // name and write each entry back through the engine. let formValues: [String: String] = [ "tag": "Welcome Back", "tagline": "Built from form input", ] for (key, value) in formValues where variableNames.contains(key) { try engine.variable.set(key: key, value: value) } ``` ### Validating Before Export Before exporting, confirm every variable the form exposes has a value. Read each one with `engine.variable.get(key:)` and block the export while any are empty, then render the finished design with `engine.block.export(_:mimeType:)`. ```swift highlight-formBasedEditing-validate // Before exporting, confirm every variable the form exposes has a value. let missingFields = try variableNames.filter { try engine.variable.get(key: $0).isEmpty } guard missingFields.isEmpty else { print("Cannot export — required fields are empty:", missingFields) return } let exported = try await engine.block.export(page, mimeType: .png) print("Exported personalized template:", exported.count, "bytes") ``` ## Error Handling Engine calls throw, so handle failures where they can occur: - **Missing values**: Validate before export and tell users which fields are required. - **Invalid images**: Check a file's type before assigning it to a placeholder fill. - **Unreachable files**: Handle failures when loading images from a URL. - **Unknown variables**: `engine.variable.get(key:)` throws if the key was never defined — guard against keys your form does not recognize. ## API Reference | Method | Description | |--------|-------------| | `engine.variable.findAll()` | List every variable name defined in the template | | `engine.variable.get(key:)` | Read a variable's current value | | `engine.variable.set(key:value:)` | Set or update a variable's value | | `engine.block.referencesAnyVariables(_:)` | Check whether a block depends on variables | | `engine.block.find(byType:)` | Find blocks by type, such as `.graphic` | | `engine.block.isPlaceholderEnabled(_:)` | Check whether a block is an editable placeholder | | `engine.block.findAllPlaceholders()` | Return every placeholder block in the scene | | `engine.block.getFill(_:)` | Get the fill block of a design block | | `engine.block.getURL(_:property:)` | Read a URL property, such as an image fill's file URI | | `engine.block.setURL(_:property:value:)` | Set a URL property to replace placeholder content | | `engine.block.export(_:mimeType:)` | Export the finished design as image data | ## Next Steps - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) — Deep dive into variable management. - [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) — Understand placeholder configuration. - [Lock the Template](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) — Combine forms with locked designs. - [Set Editing Constraints](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) — Fine-tune what users can modify. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Placeholders" description: "Use placeholders to mark editable image, video, or text areas within a locked template layout." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content-53fad7/) > [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) --- ```swift file=@cesdk_swift_examples/engine-guides-placeholders/Placeholders.swift reference-only import Foundation import IMGLYEngine @MainActor func placeholders(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let sampleImage1 = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") let sampleImage2 = baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg") // Build a sample template with an image placeholder, a text placeholder, and a // second "featured" image. This setup runs before any placeholder API is // called, so every creation call succeeds. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1200) try engine.block.setHeight(page, value: 800) try engine.block.appendChild(to: scene, child: page) // A graphic block backed by an image fill — the primary placeholder. let imagePlaceholder = try engine.block.create(.graphic) try engine.block.setShape(imagePlaceholder, shape: engine.block.createShape(.rect)) let imageSetupFill = try engine.block.createFill(.image) try engine.block.setURL(imageSetupFill, property: "fill/image/imageFileURI", value: sampleImage1) try engine.block.setFill(imagePlaceholder, fill: imageSetupFill) try engine.block.setPositionX(imagePlaceholder, value: 80) try engine.block.setPositionY(imagePlaceholder, value: 250) try engine.block.setWidth(imagePlaceholder, value: 300) try engine.block.setHeight(imagePlaceholder, value: 300) try engine.block.appendChild(to: page, child: imagePlaceholder) // A text block — the text placeholder. let textPlaceholder = try engine.block.create(.text) try engine.block.setString(textPlaceholder, property: "text/text", value: "Your headline here") try engine.block.setFloat(textPlaceholder, property: "text/fontSize", value: 48) try engine.block.setPositionX(textPlaceholder, value: 80) try engine.block.setPositionY(textPlaceholder, value: 120) try engine.block.setWidth(textPlaceholder, value: 600) try engine.block.setHeight(textPlaceholder, value: 80) try engine.block.appendChild(to: page, child: textPlaceholder) // A second graphic block used to demonstrate the combined "Act as Placeholder" setup. let featuredImage = try engine.block.create(.graphic) try engine.block.setShape(featuredImage, shape: engine.block.createShape(.rect)) let featuredSetupFill = try engine.block.createFill(.image) try engine.block.setURL(featuredSetupFill, property: "fill/image/imageFileURI", value: sampleImage2) try engine.block.setFill(featuredImage, fill: featuredSetupFill) try engine.block.setPositionX(featuredImage, value: 440) try engine.block.setPositionY(featuredImage, value: 250) try engine.block.setWidth(featuredImage, value: 300) try engine.block.setHeight(featuredImage, value: 300) try engine.block.appendChild(to: page, child: featuredImage) let imageFill = try engine.block.getFill(imagePlaceholder) let supportsBehavior = try engine.block.supportsPlaceholderBehavior(imageFill) let supportsControls = try engine.block.supportsPlaceholderControls(imagePlaceholder) print("Image fill supports placeholder behavior:", supportsBehavior) // true print("Image block supports placeholder controls:", supportsControls) // true if try engine.block.supportsPlaceholderBehavior(imageFill) { try engine.block.setPlaceholderBehaviorEnabled(imageFill, enabled: true) } let behaviorEnabled = try engine.block.isPlaceholderBehaviorEnabled(imageFill) print("Placeholder behavior enabled on the image fill:", behaviorEnabled) // true if try engine.block.supportsPlaceholderBehavior(textPlaceholder) { try engine.block.setPlaceholderBehaviorEnabled(textPlaceholder, enabled: true) } try engine.block.setPlaceholderEnabled(imagePlaceholder, enabled: true) let isInteractive = try engine.block.isPlaceholderEnabled(imagePlaceholder) print("Placeholder is interactive in Adopter mode:", isInteractive) // true let featuredFill = try engine.block.getFill(featuredImage) if try engine.block.supportsPlaceholderBehavior(featuredFill) { try engine.block.setPlaceholderBehaviorEnabled(featuredFill, enabled: true) } if try engine.block.supportsPlaceholderControls(featuredImage) { try engine.block.setPlaceholderControlsOverlayEnabled(featuredImage, enabled: true) try engine.block.setPlaceholderControlsButtonEnabled(featuredImage, enabled: true) } try engine.block.setPlaceholderControlsOverlayEnabled(imagePlaceholder, enabled: true) try engine.block.setPlaceholderControlsButtonEnabled(imagePlaceholder, enabled: true) try engine.block.setScopeEnabled(imagePlaceholder, key: "fill/change", enabled: true) try engine.block.setScopeEnabled(imagePlaceholder, key: "fill/changeType", enabled: true) try engine.block.setScopeEnabled(imagePlaceholder, key: "layer/crop", enabled: true) try engine.block.setScopeEnabled(textPlaceholder, key: "text/edit", enabled: true) try engine.block.setScopeEnabled(textPlaceholder, key: "text/character", enabled: true) for url in [sampleImage1, sampleImage2] { let slot = try engine.block.create(.graphic) try engine.block.setShape(slot, shape: engine.block.createShape(.rect)) let slotFill = try engine.block.createFill(.image) try engine.block.setURL(slotFill, property: "fill/image/imageFileURI", value: url) try engine.block.setFill(slot, fill: slotFill) try engine.block.appendChild(to: page, child: slot) try engine.block.setPlaceholderEnabled(slot, enabled: true) if try engine.block.supportsPlaceholderBehavior(slotFill) { try engine.block.setPlaceholderBehaviorEnabled(slotFill, enabled: true) } } } ``` Placeholders turn design blocks into drop-zones that users can swap content into while the template's layout and styling stay locked. This guide configures placeholder behavior and visual controls for image and text blocks with the Swift Engine API. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-placeholders) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Placeholders are the backbone of editable-yet-locked templates: a designer marks which blocks an end user may replace, and the rest of the design stays fixed. This guide covers checking placeholder support, enabling behavior, exposing visual controls, the scopes placeholders depend on, and applying settings to multiple blocks at once. The example builds a sample template with an `imagePlaceholder` graphic block, a `textPlaceholder` text block, and a second `featuredImage` graphic block. The snippets below configure their placeholder settings. ## Placeholder Fundamentals Placeholders convert design blocks into interactive drop-zones where content can be replaced while the surrounding layout and styling remain under the template author's control. ### Two Distinct Features **Placeholder behavior** marks a block's content as replaceable — it turns the block into a drop-zone and backs the support checks used to validate replacement. **Placeholder controls** are the visual affordances drawn over a placeholder: an overlay pattern and a Replace button that guide users to the editable area. These appear only inside the editor — an exported design never includes them. ### Block-Level vs Fill-Level Behavior The target of the behavior API depends on the block type: - **Graphic blocks** (images and videos) hold their replaceable content in a **fill**. Enable placeholder behavior on the fill from `engine.block.getFill(_:)`, and enable placeholder controls on the block. - **Text blocks** have no replaceable fill, so enable placeholder behavior directly on the block. Text blocks do not support placeholder controls. ## Checking Placeholder Support Before enabling placeholder features, check whether the target supports them. For a graphic block, query the fill for behavior support and the block for controls support. ```swift highlight-placeholders-checkSupport let imageFill = try engine.block.getFill(imagePlaceholder) let supportsBehavior = try engine.block.supportsPlaceholderBehavior(imageFill) let supportsControls = try engine.block.supportsPlaceholderControls(imagePlaceholder) print("Image fill supports placeholder behavior:", supportsBehavior) // true print("Image block supports placeholder controls:", supportsControls) // true ``` `supportsPlaceholderBehavior(_:)` reports whether a fill — or a text block — can become a drop-zone. `supportsPlaceholderControls(_:)` reports whether a block can display the overlay and button; it returns `false` for text blocks. ## Enabling Placeholder Behavior Enabling placeholder behavior is what turns a block into a drop-zone. The target differs by block type. ### For Graphic Blocks (Images/Videos) For graphic blocks, enable behavior on the fill obtained above with `engine.block.getFill(_:)`, not on the block itself. ```swift highlight-placeholders-enableBehaviorGraphic if try engine.block.supportsPlaceholderBehavior(imageFill) { try engine.block.setPlaceholderBehaviorEnabled(imageFill, enabled: true) } let behaviorEnabled = try engine.block.isPlaceholderBehaviorEnabled(imageFill) print("Placeholder behavior enabled on the image fill:", behaviorEnabled) // true ``` Calling `setPlaceholderBehaviorEnabled(_:enabled:)` on the fill reflects the underlying architecture: a graphic block contains a fill, and the fill is the replaceable content. The matching query `isPlaceholderBehaviorEnabled(_:)` reads back the current state. ### For Text Blocks For text blocks, enable behavior directly on the block — text blocks carry their content rather than a replaceable fill. ```swift highlight-placeholders-enableBehaviorText if try engine.block.supportsPlaceholderBehavior(textPlaceholder) { try engine.block.setPlaceholderBehaviorEnabled(textPlaceholder, enabled: true) } ``` ## Enabling Adopter Mode Interaction Placeholder behavior marks content as replaceable, but a block also has to be enabled for interaction in the Adopter role before a user can swap its content. ```swift highlight-placeholders-enableAdopterMode try engine.block.setPlaceholderEnabled(imagePlaceholder, enabled: true) let isInteractive = try engine.block.isPlaceholderEnabled(imagePlaceholder) print("Placeholder is interactive in Adopter mode:", isInteractive) // true ``` `setPlaceholderEnabled(_:enabled:)` controls whether the placeholder is interactive for users in the Adopter role. CE.SDK distinguishes the **Creator** role (full editing access) from the **Adopter** role (replace-only). In the Creator role, enabling the placeholder also opens the block's `editor/select` scope so the block stays selectable. Once an Adopter replaces the content, the engine clears the placeholder flag automatically. ### Automatic Management In the CE.SDK editor, placeholder interaction is managed for you: the editor keeps `setPlaceholderEnabled(_:enabled:)` in sync with a block's content scopes as they change. When you configure placeholders directly through the Engine, as in this guide, call `setPlaceholderEnabled(_:enabled:)` explicitly to make a placeholder interactive. ## Configuring Visual Feedback Placeholders can display visual indicators that point users to the editable area. ### Combined Setup: The "Act as Placeholder" Pattern The most common setup enables placeholder behavior on the fill and both visual controls on the block together. ```swift highlight-placeholders-fullConfiguration let featuredFill = try engine.block.getFill(featuredImage) if try engine.block.supportsPlaceholderBehavior(featuredFill) { try engine.block.setPlaceholderBehaviorEnabled(featuredFill, enabled: true) } if try engine.block.supportsPlaceholderControls(featuredImage) { try engine.block.setPlaceholderControlsOverlayEnabled(featuredImage, enabled: true) try engine.block.setPlaceholderControlsButtonEnabled(featuredImage, enabled: true) } ``` This is the recommended starting point: it makes the block replaceable and shows both the overlay and the button in one pass. ### Individual Control Options You can also toggle each control independently. The **overlay pattern** is a dotted surface that marks the drop-zone: ```swift highlight-placeholders-enableOverlay try engine.block.setPlaceholderControlsOverlayEnabled(imagePlaceholder, enabled: true) ``` The **Replace button** is a single-tap entry point for swapping content: ```swift highlight-placeholders-enableButton try engine.block.setPlaceholderControlsButtonEnabled(imagePlaceholder, enabled: true) ``` `isPlaceholderControlsOverlayEnabled(_:)` and `isPlaceholderControlsButtonEnabled(_:)` return the current visibility of each control. ## Scope Requirements and Dependencies Whether an Adopter can actually replace a placeholder's content depends on the block's scopes. A graphic placeholder is replaceable only when `fill/change` allows it; a text placeholder is editable only when `text/edit` allows it. ```swift highlight-placeholders-scopes try engine.block.setScopeEnabled(imagePlaceholder, key: "fill/change", enabled: true) try engine.block.setScopeEnabled(imagePlaceholder, key: "fill/changeType", enabled: true) try engine.block.setScopeEnabled(imagePlaceholder, key: "layer/crop", enabled: true) try engine.block.setScopeEnabled(textPlaceholder, key: "text/edit", enabled: true) try engine.block.setScopeEnabled(textPlaceholder, key: "text/character", enabled: true) ``` Optional scopes broaden what an Adopter can change: - `fill/changeType` — switch between image, video, and solid-color fills. - `layer/crop` — crop replacement images. - `text/character` — change font and character formatting on text placeholders. ## Working with Multiple Placeholders Templates often have several content slots. Apply placeholder settings systematically by looping over the blocks. ```swift highlight-placeholders-batchOperation for url in [sampleImage1, sampleImage2] { let slot = try engine.block.create(.graphic) try engine.block.setShape(slot, shape: engine.block.createShape(.rect)) let slotFill = try engine.block.createFill(.image) try engine.block.setURL(slotFill, property: "fill/image/imageFileURI", value: url) try engine.block.setFill(slot, fill: slotFill) try engine.block.appendChild(to: page, child: slot) try engine.block.setPlaceholderEnabled(slot, enabled: true) if try engine.block.supportsPlaceholderBehavior(slotFill) { try engine.block.setPlaceholderBehaviorEnabled(slotFill, enabled: true) } } ``` This pattern suits collage templates, product showcases, and any layout with multiple content slots. ## API Reference | Method | Description | |--------|-------------| | `engine.block.supportsPlaceholderBehavior(_:)` | Checks whether a block or fill supports placeholder behavior | | `engine.block.setPlaceholderBehaviorEnabled(_:enabled:)` | Enables or disables placeholder behavior for a block or fill | | `engine.block.isPlaceholderBehaviorEnabled(_:)` | Queries whether placeholder behavior is enabled | | `engine.block.setPlaceholderEnabled(_:enabled:)` | Enables or disables placeholder interaction in Adopter mode | | `engine.block.isPlaceholderEnabled(_:)` | Queries whether placeholder interaction is enabled | | `engine.block.supportsPlaceholderControls(_:)` | Checks whether a block supports placeholder controls | | `engine.block.setPlaceholderControlsOverlayEnabled(_:enabled:)` | Enables or disables the placeholder overlay pattern | | `engine.block.isPlaceholderControlsOverlayEnabled(_:)` | Queries whether the overlay pattern is shown | | `engine.block.setPlaceholderControlsButtonEnabled(_:enabled:)` | Enables or disables the placeholder button | | `engine.block.isPlaceholderControlsButtonEnabled(_:)` | Queries whether the placeholder button is shown | ## Next Steps - [Lock the Template](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) - Restrict editing access to specific elements or properties to enforce design rules - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) - Define dynamic text elements that can be populated with custom values --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Set Editing Constraints" description: "Control editing capabilities in CE.SDK templates with the Scope system to lock positions, prevent transformations, and build guided editing experiences in Swift." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/set-editing-constraints-c892c0/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content-53fad7/) > [Set Editing Constraints](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) --- ```swift file=@cesdk_swift_examples/engine-guides-set-editing-constraints/SetEditingConstraints.swift reference-only import Foundation import IMGLYEngine @MainActor func setEditingConstraints(engine: Engine) throws { // Demo scaffolding: a scene and page to hold the constrained blocks. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1200) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) try engine.editor.setGlobalScope(key: "layer/move", value: .defer) try engine.editor.setGlobalScope(key: "layer/resize", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/destroy", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/duplicate", value: .defer) // Demo scaffolding: two renderable graphic blocks, constrained independently. let positionLocked = try engine.block.create(.graphic) try engine.block.setShape(positionLocked, shape: engine.block.createShape(.rect)) try engine.block.setFill(positionLocked, fill: engine.block.createFill(.color)) try engine.block.setWidth(positionLocked, value: 200) try engine.block.setHeight(positionLocked, value: 200) try engine.block.appendChild(to: page, child: positionLocked) let deletionLocked = try engine.block.create(.graphic) try engine.block.setShape(deletionLocked, shape: engine.block.createShape(.rect)) try engine.block.setFill(deletionLocked, fill: engine.block.createFill(.color)) try engine.block.setWidth(deletionLocked, value: 200) try engine.block.setHeight(deletionLocked, value: 200) try engine.block.appendChild(to: page, child: deletionLocked) try engine.block.setScopeEnabled(positionLocked, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(positionLocked, key: "layer/resize", enabled: true) try engine.block.setScopeEnabled(deletionLocked, key: "lifecycle/destroy", enabled: false) try engine.block.setScopeEnabled(deletionLocked, key: "lifecycle/duplicate", enabled: false) try engine.block.setScopeEnabled(deletionLocked, key: "layer/move", enabled: true) try engine.block.setScopeEnabled(deletionLocked, key: "layer/resize", enabled: true) let canMove = try engine.block.isScopeEnabled(positionLocked, key: "layer/move") print("layer/move enabled at block level: \(canMove)") // false let moveAllowed = try engine.block.isAllowedByScope(positionLocked, key: "layer/move") print("layer/move allowed: \(moveAllowed)") // false } ``` Control what users can edit in templates by setting fine-grained permissions on individual blocks or globally across your scene using the CE.SDK Scope system. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-set-editing-constraints) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Editing constraints let you lock specific properties of design elements while keeping others editable. The Scope system provides granular control over more than 20 editing capabilities, including movement, resizing, rotation, fill changes, text editing, and lifecycle operations. Use it to create brand templates, guided editing experiences, and form-based workflows where design integrity must be preserved while still allowing controlled personalization. ## Understanding Scopes ### What are Scopes? A scope is a permission key that controls a specific editing capability. Each scope represents a distinct action, such as moving blocks (`"layer/move"`), changing fills (`"fill/change"`), or editing text content (`"text/edit"`). By enabling or disabling scopes, you control exactly what users can and cannot do with each design element. Scopes exist at two levels: - **Block-level scopes**: Per-block permissions set with `setScopeEnabled(_:key:enabled:)`. - **Global scopes**: Default behavior for all blocks set with `setGlobalScope(key:value:)`. These starting values depend on the editor role, which you set with `setRole(_:)`. Under the default Creator role, every global scope is `.allow`, so every action is permitted and block-level scopes are not consulted. Blocks created in this role start with their block-level scopes disabled — a setting that only takes effect once a scope defers to the block level, either by switching to the Adopter role (which defers global scopes to the block level) or by deferring individual global scopes yourself, as the rest of this guide does. ### Available Scope Categories CE.SDK groups scopes into logical categories. Retrieve the full list at runtime with `engine.editor.findAllScopes()`. | Category | Purpose | Example Scopes | | --- | --- | --- | | **Text Editing** | Control text content and formatting | `text/edit`, `text/character` | | **Fill & Stroke** | Manage colors and gradients | `fill/change`, `fill/changeType`, `stroke/change` | | **Shape** | Modify shape properties | `shape/change` | | **Layer Transform** | Control position and dimensions | `layer/move`, `layer/resize`, `layer/rotate`, `layer/flip`, `layer/crop` | | **Layer Appearance** | Manage visual properties | `layer/opacity`, `layer/blendMode`, `layer/visibility` | | **Effects & Filters** | Apply visual effects | `appearance/adjustments`, `appearance/filter`, `appearance/effect`, `appearance/blur`, `appearance/shadow` | | **Lifecycle** | Control creation and deletion | `lifecycle/destroy`, `lifecycle/duplicate` | | **Editor** | Manage scene-level actions | `editor/add`, `editor/select` | ## Scope Configuration ### Global Scope Modes Global scopes set the default behavior for all blocks in the scene. They have three modes: | Mode | Behavior | | --- | --- | | `.allow` | Always allow the action, overriding block-level settings | | `.deny` | Always deny the action, overriding block-level settings | | `.defer` | Use the block-level setting for each block | To make block-level constraints take effect under the default Creator role, defer the relevant global scopes to the block level: ```swift highlight-setEditingConstraints-globalScopes try engine.editor.setGlobalScope(key: "layer/move", value: .defer) try engine.editor.setGlobalScope(key: "layer/resize", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/destroy", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/duplicate", value: .defer) ``` ### Scope Resolution Priority When both global and block-level scopes apply, they resolve in this order: 1. **Global `.deny`** takes highest priority — the action is always denied. 2. **Global `.allow`** takes second priority — the action is always allowed. 3. **Global `.defer`** uses the block-level setting for each block. ## Setting Block-Level Constraints ### Locking Position Prevent users from moving a block while keeping other edits available: ```swift highlight-setEditingConstraints-lockPosition try engine.block.setScopeEnabled(positionLocked, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(positionLocked, key: "layer/resize", enabled: true) ``` Disabling `layer/move` locks the block's position. Because deferring a scope makes the engine consult the block-level setting — and block-level scopes start disabled — explicitly enable `layer/resize` so resizing stays available. Scopes you did not defer (such as `fill/change` and `layer/rotate`) remain at their global `.allow` default and stay editable. ### Preventing Deletion Protect a block from being deleted or duplicated: ```swift highlight-setEditingConstraints-preventDeletion try engine.block.setScopeEnabled(deletionLocked, key: "lifecycle/destroy", enabled: false) try engine.block.setScopeEnabled(deletionLocked, key: "lifecycle/duplicate", enabled: false) try engine.block.setScopeEnabled(deletionLocked, key: "layer/move", enabled: true) try engine.block.setScopeEnabled(deletionLocked, key: "layer/resize", enabled: true) ``` Disabling `lifecycle/destroy` and `lifecycle/duplicate` keeps the block in the template. Enabling `layer/move` and `layer/resize` keeps those deferred capabilities available, so the block stays movable and resizable while it cannot be removed. Use this for essential template elements that must remain present. ### Checking Scope State Query the block-level setting for any scope: ```swift highlight-setEditingConstraints-checkScope let canMove = try engine.block.isScopeEnabled(positionLocked, key: "layer/move") print("layer/move enabled at block level: \(canMove)") // false ``` `isScopeEnabled(_:key:)` returns whether the scope is enabled at the block level. It does not consider the global scope. ### Checking Effective Permissions Check the effective permission, which resolves both the block-level and global settings: ```swift highlight-setEditingConstraints-checkAllowed let moveAllowed = try engine.block.isAllowedByScope(positionLocked, key: "layer/move") print("layer/move allowed: \(moveAllowed)") // false ``` `isAllowedByScope(_:key:)` returns the final permission after applying the resolution priority above. Use it when you need to know whether an action is actually permitted. ## API Reference | Method | Description | | --- | --- | | `engine.block.setScopeEnabled(_:key:enabled:)` | Enable or disable a scope for a specific block | | `engine.block.isScopeEnabled(_:key:)` | Check whether a scope is enabled at the block level | | `engine.block.isAllowedByScope(_:key:)` | Check whether a scope is allowed, considering both block and global settings | | `engine.editor.setGlobalScope(key:value:)` | Set the global scope policy (`.allow`, `.deny`, or `.defer`) | | `engine.editor.findAllScopes()` | List all available scope keys | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Text Variables" description: "Define dynamic text elements that can be populated with custom values at runtime." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content-53fad7/) > [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-variables/TextVariables.swift reference-only import Foundation import IMGLYEngine @MainActor func textVariables(engine: Engine) async throws { // Clear any leftover sample keys so the example is idempotent on repeat runs. for key in ["firstName", "lastName"] where engine.variable.findAll().contains(key) { try engine.variable.remove(key: key) } // Build a certificate-sized template page to hold the tokenized heading. // A Pixel design unit interprets the width, height, and font sizes below in // pixels, so the heading renders at a predictable size. let scene = try engine.scene.create(designUnit: .px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 260) try engine.block.appendChild(to: scene, child: page) let textBlock = try engine.block.create(.text) try engine.block.replaceText(textBlock, text: "Certificate for {{firstName}} {{lastName}}") try engine.block.setPositionX(textBlock, value: 60) try engine.block.setPositionY(textBlock, value: 105) try engine.block.setWidth(textBlock, value: 680) try engine.block.setHeightMode(textBlock, mode: .auto) try engine.block.setTextFontSize(textBlock, fontSize: 42) try engine.block.setTextColor(textBlock, color: .rgba(r: 0.078, g: 0.09, b: 0.122, a: 1)) try engine.block.appendChild(to: page, child: textBlock) let recipient = ["firstName": "Alex", "lastName": "Smith"] for (key, value) in recipient { try engine.variable.set(key: key, value: value) } // With both variables seeded, the page renders the resolved heading. try await engine.captureGuide(page, label: "hero") let variableNames = engine.variable.findAll().sorted() print("Stored variables:", variableNames) // ["firstName", "lastName"] let firstName = try engine.variable.get(key: "firstName") print("firstName:", firstName) // "Alex" let hasVariableReferences = try engine.block.referencesAnyVariables(textBlock) print("Heading references variables:", hasVariableReferences) // true let tokenPattern = try NSRegularExpression(pattern: #"\{\{\s*([^{}]+?)\s*\}\}"#) let tokenKeys = try engine.block.find(byType: .text).flatMap { block -> [String] in let content = try engine.block.getString(block, property: "text/text") let range = NSRange(content.startIndex ..< content.endIndex, in: content) return tokenPattern.matches(in: content, range: range).compactMap { match in Range(match.range(at: 1), in: content).map { String(content[$0]) } } } print("Tokens in scene:", tokenKeys) // ["firstName", "lastName"] try engine.variable.remove(key: "lastName") let remainingVariables = engine.variable.findAll().sorted() print("Variables after removal:", remainingVariables) // ["firstName"] } ``` Create reusable templates whose text content is populated from data at runtime. Text variables separate a design's fixed layout from the values your app supplies, so a single template can produce many personalized results. ![A certificate heading that reads "Certificate for Alex Smith" — the \{\{firstName}} and \{\{lastName}} tokens resolved to their variable values when the page rendered.](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-text-variables) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Text variables separate a design's text layout from the values your app supplies. Put tokens such as `{{firstName}}` into text blocks, then update the variable store with matching keys before you preview or export. Color, number, image and typeface variables drive block properties instead of text. See [Variable Bindings](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/variable-bindings-faa12f/). This guide covers the Swift Engine APIs for creating tokenized text, setting values, reading them back, detecting which blocks reference variables, and scanning a scene for token names. ## Variables and Tokens Tokens and variables are related, but they are not the same thing. A token is the `{{key}}` placeholder written into a text block, while a variable is the key-value entry of the current scene. Tokens do not automatically create variable entries. Use `engine.variable.set(key:value:)` to seed the store before you expect a token to resolve — an unresolved token renders as its literal `{{key}}` text. ## Binding Tokens to Text Blocks Use the same text editing APIs you already use for normal text blocks. The token syntax can stand on its own or sit inside a longer string. Add the text block to the page that belongs to your template scene so it becomes visible, exportable content. ```swift highlight-textVariables-bind-tokens let textBlock = try engine.block.create(.text) try engine.block.replaceText(textBlock, text: "Certificate for {{firstName}} {{lastName}}") try engine.block.setPositionX(textBlock, value: 60) try engine.block.setPositionY(textBlock, value: 105) try engine.block.setWidth(textBlock, value: 680) try engine.block.setHeightMode(textBlock, mode: .auto) try engine.block.setTextFontSize(textBlock, fontSize: 42) try engine.block.setTextColor(textBlock, color: .rgba(r: 0.078, g: 0.09, b: 0.122, a: 1)) try engine.block.appendChild(to: page, child: textBlock) ``` The stored `text/text` value stays the template string. When the engine renders the block, matching variable values replace the token positions. ## Setting Variable Values Populate the variable store with `engine.variable.set(key:value:)`. Calling `set` for an existing key updates the value, so the same template can be reused for multiple data records. ```swift highlight-textVariables-set-values let recipient = ["firstName": "Alex", "lastName": "Smith"] for (key, value) in recipient { try engine.variable.set(key: key, value: value) } ``` Variable keys are case-sensitive. Keep the key names in your data model and text tokens identical. ## Discovering Variables Use `engine.variable.findAll()` to list the keys of the current scene. This reports variables that have been set, not every token that appears in text. It is non-throwing and returns a `[String]`. ```swift highlight-textVariables-discover-variables let variableNames = engine.variable.findAll().sorted() print("Stored variables:", variableNames) // ["firstName", "lastName"] ``` ## Reading Variable Values Read an existing value with `engine.variable.get(key:)`. Call it once you know the key exists — for example from your own data model or from `findAll()`. ```swift highlight-textVariables-read-variable let firstName = try engine.variable.get(key: "firstName") print("firstName:", firstName) // "Alex" ``` If the key is missing, the engine reports an error instead of returning an empty string. ## Detecting Variable References Use `engine.block.referencesAnyVariables(_:)` to check whether a text block contains any `{{...}}` tokens. ```swift highlight-textVariables-detect-references let hasVariableReferences = try engine.block.referencesAnyVariables(textBlock) print("Heading references variables:", hasVariableReferences) // true ``` The check applies to the block you pass in. To validate a whole scene, iterate over the text blocks and check each one. ## Scanning Token Names Because `findAll()` lists stored variables only, scan text block content when you need to discover token names that have not been seeded yet. Read the `text/text` property from each text block returned by `engine.block.find(byType: .text)` and extract the tokens with a regular expression. ```swift highlight-textVariables-scan-tokens let tokenPattern = try NSRegularExpression(pattern: #"\{\{\s*([^{}]+?)\s*\}\}"#) let tokenKeys = try engine.block.find(byType: .text).flatMap { block -> [String] in let content = try engine.block.getString(block, property: "text/text") let range = NSRange(content.startIndex ..< content.endIndex, in: content) return tokenPattern.matches(in: content, range: range).compactMap { match in Range(match.range(at: 1), in: content).map { String(content[$0]) } } } print("Tokens in scene:", tokenKeys) // ["firstName", "lastName"] ``` The pattern scans the text between `{{` and `}}`, tolerating inner whitespace, so it also handles names such as `user.name`, `campaign-id`, or `full_name`. ## Removing Variables Remove a variable with `engine.variable.remove(key:)` when it is no longer part of the current scene or data record. ```swift highlight-textVariables-remove-variable try engine.variable.remove(key: "lastName") let remainingVariables = engine.variable.findAll().sorted() print("Variables after removal:", remainingVariables) // ["firstName"] ``` Removing a variable does not remove tokens from text blocks. If a block still contains `{{lastName}}`, that token remains in the template text until you change the text content or set the variable again. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.create(.text)` | Create a text block for tokenized copy | | `engine.block.replaceText(_:text:)` | Set text content, including `{{key}}` tokens | | `engine.block.setPositionX(_:value:)` / `setPositionY(_:value:)` | Position the text block on the template page | | `engine.block.setWidth(_:value:)` | Set the text block width inside the page | | `engine.block.setHeightMode(_:mode:)` | Let the text block height adapt to the inserted copy | | `engine.block.setTextFontSize(_:fontSize:)` | Set the text block font size | | `engine.block.setTextColor(_:color:)` | Set the text block color | | `engine.block.appendChild(to:child:)` | Attach the tokenized text block to the page | | `engine.variable.set(key:value:)` | Create or update a text variable value | | `engine.variable.findAll()` | List the variable keys of the current scene | | `engine.variable.get(key:)` | Read an existing variable value | | `engine.variable.remove(key:)` | Remove an existing variable value | | `engine.block.referencesAnyVariables(_:)` | Check whether one block contains variable tokens | | `engine.block.find(byType:)` | Find text blocks for scene-level scanning | | `engine.block.getString(_:property:)` | Read a text block's stored template string | ### Properties | Property | Type | Description | | --- | --- | --- | | `text/text` | String | The block's stored template string, including `{{key}}` tokens | ## Troubleshooting **A token appears in the output**: Confirm the token name exactly matches a key passed to `engine.variable.set(key:value:)`, including case. **`findAll()` returns fewer names than expected**: `findAll()` lists stored variables only. It does not scan text blocks for tokens. **`get(key:)` fails for a variable**: Check that the key exists before reading it. You can seed optional variables with an empty string when you want unresolved text to disappear. **`referencesAnyVariables(_:)` returns `false`**: Pass the actual text block. The method does not inspect the children of a parent block. ## Next Steps - [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) — Mark editable media or text areas inside locked template layouts. - [Form-Based Editing](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/form-based-editing-a8a779/) — Expose variables and placeholders through custom input controls. - [Data Merge](https://img.ly/docs/cesdk/mac-catalyst/automation/data-merge-ae087c/) — Merge external records into templates with variables and named placeholder blocks. - [Templating](https://img.ly/docs/cesdk/mac-catalyst/concepts/templating-f94385/) — Understand reusable scene templates with dynamic text and placeholder media. - [Automate Design Generation](https://img.ly/docs/cesdk/mac-catalyst/automation/design-generation-98a99e/) — Generate on-brand designs programmatically using templates and variables. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Value Sets" description: "Keep named alternative values for scene variables in value sets, and switch the values that the scene shows." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/value-sets-fa0904/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content-53fad7/) > [Value Sets](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/value-sets-fa0904/) --- ```swift file=@cesdk_swift_examples/engine-guides-value-sets/ValueSets.swift reference-only import Foundation import IMGLYEngine struct ValueSetsResult { let valueSets: [ValueSetID] let valueSetName: String let shownName: String let defaultName: String let shownAccent: Color let writtenName: String let defaultAfterWrite: String let cardColor: Color let activeSetKeepsID: Bool let foundSetKeepsID: Bool let cityAfterLoad: String let missingVariableErrorCode: EngineErrorCode? let nameTakenErrorCode: EngineErrorCode? let activeSetAfterDestroy: ValueSetID? let nameAfterDestroy: String } @MainActor func valueSets(engine: Engine) async throws -> ValueSetsResult { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // Scene variables belong to the scene. A save writes them, and undo reverts them. try engine.variable.set(key: "first_name", value: "Ada") try engine.variable.set(key: "accent", color: .rgba(r: 0.48, g: 0.23, b: 0.93, a: 1)) let card = try engine.block.create(.graphic) try engine.block.setShape(card, shape: engine.block.createShape(.rect)) try engine.block.setFill(card, fill: engine.block.createFill(.color)) try engine.block.setWidth(card, value: 720) try engine.block.setHeight(card, value: 520) try engine.block.setPositionX(card, value: 40) try engine.block.setPositionY(card, value: 40) try engine.block.appendChild(to: page, child: card) let cardFill = try engine.block.getFill(card) try engine.block.setVariableBinding(cardFill, property: "fill/color/value", key: "accent") let text = try engine.block.create(.text) try engine.block.replaceText(text, text: "Hello {{first_name}}") try engine.block.setWidth(text, value: 640) try engine.block.setHeightMode(text, mode: .auto) try engine.block.setPositionX(text, value: 80) try engine.block.setPositionY(text, value: 80) try engine.block.appendChild(to: page, child: text) // A value set holds alternative values for some scene variables. let longNames = try engine.variable.createValueSet(name: "Long names") try engine.variable.set(key: "first_name", value: "Maximiliane Huber-Schwarzenberg", target: .valueSet(longNames)) let valueSets = engine.variable.findAllValueSets() // [longNames] let valueSetName = try engine.variable.getValueSetName(longNames) // "Long names" // The active set wins over the default values in text, bound properties and getters. try engine.variable.setActiveValueSet(longNames) let shownName = try engine.variable.get(key: "first_name") // "Maximiliane Huber-Schwarzenberg" let defaultName = try engine.variable.get(key: "first_name", target: .defaultValue) // "Ada" // A variable that the set has no value for shows its default. let shownAccent = try engine.variable.getColor(key: "accent") // the purple default // A setter without a target writes the layer that the getter reads: the active set // when it holds the key, else the default value. try engine.variable.set(key: "first_name", value: "Maximiliane Huber") let writtenName = try engine.variable.get(key: "first_name") // "Maximiliane Huber" let defaultAfterWrite = try engine.variable.get(key: "first_name", target: .defaultValue) // "Ada" try engine.variable.set(key: "accent", color: .rgba(r: 0.95, g: 0.45, b: 0.1, a: 1)) let cardColor: Color = try engine.block.getColor(cardFill, property: "fill/color/value") // the new orange default // Exports show the values of the active set, as the canvas does. let png = try await engine.block.export(page, mimeType: .png) print("Exported \(png.count) bytes") // A save holds the default values, the value sets and the active set. let saved = try await engine.scene.saveToString() try await engine.scene.load(from: saved) // The scene saves the ID of a set, so it stays the same across a load. let activeSetKeepsID = engine.variable.getActiveValueSet() == longNames // true let foundSetKeepsID = engine.variable.findValueSet(byName: "Long names") == longNames // true // A variable set while no scene exists waits for the next scene. That scene takes it over // its own value, for example to fill a template with the data of one record. if let loadedScene = try engine.scene.get() { try engine.block.destroy(loadedScene) } try engine.variable.set(key: "city", value: "Berlin") try await engine.scene.load(from: saved) let cityAfterLoad = try engine.variable.get(key: "city") // "Berlin" var missingVariableErrorCode: EngineErrorCode? do { // A value set holds values only for variables of the scene. try engine.variable.set(key: "missing", value: "x", target: .valueSet(longNames)) } catch let error as EngineError { missingVariableErrorCode = error.catalogCode // .blockVariableNotFound } var nameTakenErrorCode: EngineErrorCode? do { _ = try engine.variable.createValueSet(name: "Long names") } catch let error as EngineError { nameTakenErrorCode = error.catalogCode // .blockValueSetNameTaken } // Destroying the active set shows the default values again. try engine.variable.destroyValueSet(longNames) let activeSetAfterDestroy = engine.variable.getActiveValueSet() // nil let nameAfterDestroy = try engine.variable.get(key: "first_name") // "Ada" return ValueSetsResult( valueSets: valueSets, valueSetName: valueSetName, shownName: shownName, defaultName: defaultName, shownAccent: shownAccent, writtenName: writtenName, defaultAfterWrite: defaultAfterWrite, cardColor: cardColor, activeSetKeepsID: activeSetKeepsID, foundSetKeepsID: foundSetKeepsID, cityAfterLoad: cityAfterLoad, missingVariableErrorCode: missingVariableErrorCode, nameTakenErrorCode: nameTakenErrorCode, activeSetAfterDestroy: activeSetAfterDestroy, nameAfterDestroy: nameAfterDestroy, ) } ``` Keep named alternative values for scene variables in value sets, and switch the values that the scene shows. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-value-sets) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. A variable gets its value from two layers. The scene saves its variables with their default values, as the [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) and [Variable Bindings](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/variable-bindings-faa12f/) guides show. A value set holds named alternative values for some of these variables, such as worst-case texts or another language. Text, bound properties, getters and exports show the value of the active value set, if it has one, else the default value. ## Scene Variables A setter writes the default value of a new scene variable, and undo reverts the change. We create a text that shows `first_name` and a card whose fill follows `accent`. ```swift highlight-valueSets-sceneVariables // Scene variables belong to the scene. A save writes them, and undo reverts them. try engine.variable.set(key: "first_name", value: "Ada") try engine.variable.set(key: "accent", color: .rgba(r: 0.48, g: 0.23, b: 0.93, a: 1)) let card = try engine.block.create(.graphic) try engine.block.setShape(card, shape: engine.block.createShape(.rect)) try engine.block.setFill(card, fill: engine.block.createFill(.color)) try engine.block.setWidth(card, value: 720) try engine.block.setHeight(card, value: 520) try engine.block.setPositionX(card, value: 40) try engine.block.setPositionY(card, value: 40) try engine.block.appendChild(to: page, child: card) let cardFill = try engine.block.getFill(card) try engine.block.setVariableBinding(cardFill, property: "fill/color/value", key: "accent") let text = try engine.block.create(.text) try engine.block.replaceText(text, text: "Hello {{first_name}}") try engine.block.setWidth(text, value: 640) try engine.block.setHeightMode(text, mode: .auto) try engine.block.setPositionX(text, value: 80) try engine.block.setPositionY(text, value: 80) try engine.block.appendChild(to: page, child: text) ``` ## Creating Value Sets `createValueSet(name:)` adds an empty set to the scene and returns its ID, a `ValueSetID`. Pass `target: .valueSet(id)` to a setter to write a value into the set. A set holds values only for scene variables, and each value has the type of its variable. ```swift highlight-valueSets-createValueSet // A value set holds alternative values for some scene variables. let longNames = try engine.variable.createValueSet(name: "Long names") try engine.variable.set(key: "first_name", value: "Maximiliane Huber-Schwarzenberg", target: .valueSet(longNames)) let valueSets = engine.variable.findAllValueSets() // [longNames] let valueSetName = try engine.variable.getValueSetName(longNames) // "Long names" ``` Set names are unique in a scene, must not be empty and must not start or end with whitespace. `setValueSetName(_:name:)` renames a set, `findAllValueSets()` lists the IDs in creation order, and `findValueSet(byName:)` finds an ID by its name. `remove(key:target:)` with `.valueSet(id)` removes one value from a set, and the variable shows its default again. ## Activating a Value Set `setActiveValueSet(_:)` makes the values of one set win over the defaults. A variable that the set has no value for shows its default. Pass `nil` to show the defaults again. A getter without a target returns the value that the scene shows. Pass `target: .defaultValue` to read only the default value. ```swift highlight-valueSets-activateValueSet // The active set wins over the default values in text, bound properties and getters. try engine.variable.setActiveValueSet(longNames) let shownName = try engine.variable.get(key: "first_name") // "Maximiliane Huber-Schwarzenberg" let defaultName = try engine.variable.get(key: "first_name", target: .defaultValue) // "Ada" // A variable that the set has no value for shows its default. let shownAccent = try engine.variable.getColor(key: "accent") // the purple default ``` The scene saves the active set, and undo reverts a switch. ## Writing Without a Target A setter without a target writes the layer that the getter reads. When the active set holds the key, the setter writes the set. Otherwise it writes the default value. So `get(key:)` always returns what `set(key:value:)` wrote. ```swift highlight-valueSets-writeThrough // A setter without a target writes the layer that the getter reads: the active set // when it holds the key, else the default value. try engine.variable.set(key: "first_name", value: "Maximiliane Huber") let writtenName = try engine.variable.get(key: "first_name") // "Maximiliane Huber" let defaultAfterWrite = try engine.variable.get(key: "first_name", target: .defaultValue) // "Ada" try engine.variable.set(key: "accent", color: .rgba(r: 0.95, g: 0.45, b: 0.1, a: 1)) let cardColor: Color = try engine.block.getColor(cardFill, property: "fill/color/value") // the new orange default ``` ## Exporting the Result An export shows the values of the active set, as the canvas does. Here the card shows `Maximiliane Huber`, which the write above put into the active set, and the orange color. ```swift highlight-valueSets-export // Exports show the values of the active set, as the canvas does. let png = try await engine.block.export(page, mimeType: .png) print("Exported \(png.count) bytes") ``` ## Saving and Loading A save writes the default values, the value sets and the active set. A load replaces the scene variables and value sets with those of the loaded scene. The scene saves the ID of each set, so an ID stays the same across saves, loads and renames. ```swift highlight-valueSets-saveAndLoad // A save holds the default values, the value sets and the active set. let saved = try await engine.scene.saveToString() try await engine.scene.load(from: saved) // The scene saves the ID of a set, so it stays the same across a load. let activeSetKeepsID = engine.variable.getActiveValueSet() == longNames // true let foundSetKeepsID = engine.variable.findValueSet(byName: "Long names") == longNames // true ``` Applying a template keeps the variables, the value sets and the active set of the current scene. It adds the variables and value sets of the template that the scene lacks. ## Setting Variables Before a Load A variable that you set while no scene exists waits for the next scene. The next created or loaded scene takes it as a setter without a target writes it. That is the active set when the set holds the key, else the default. A value of another type than the loaded variable is dropped. Use it to fill a template with the data of one record. ```swift highlight-valueSets-beforeLoad // A variable set while no scene exists waits for the next scene. That scene takes it over // its own value, for example to fill a template with the data of one record. if let loadedScene = try engine.scene.get() { try engine.block.destroy(loadedScene) } try engine.variable.set(key: "city", value: "Berlin") try await engine.scene.load(from: saved) let cityAfterLoad = try engine.variable.get(key: "city") // "Berlin" ``` ## Handling Errors A value set holds values only for variables of the scene. A second set with a taken name fails too. Each call throws an `EngineError` whose `catalogCode` names the error. ```swift highlight-valueSets-errors var missingVariableErrorCode: EngineErrorCode? do { // A value set holds values only for variables of the scene. try engine.variable.set(key: "missing", value: "x", target: .valueSet(longNames)) } catch let error as EngineError { missingVariableErrorCode = error.catalogCode // .blockVariableNotFound } var nameTakenErrorCode: EngineErrorCode? do { _ = try engine.variable.createValueSet(name: "Long names") } catch let error as EngineError { nameTakenErrorCode = error.catalogCode // .blockValueSetNameTaken } ``` ## Destroying Value Sets `destroyValueSet(_:)` removes a set and its values. When the set is active, the defaults show again. Undo restores the set with the same ID. ```swift highlight-valueSets-destroyValueSet // Destroying the active set shows the default values again. try engine.variable.destroyValueSet(longNames) let activeSetAfterDestroy = engine.variable.getActiveValueSet() // nil let nameAfterDestroy = try engine.variable.get(key: "first_name") // "Ada" ``` ## Troubleshooting - **The name of the set is taken** (`BLOCK.VALUE_SET_NAME_TAKEN`): pick a name that no set of the scene uses. - **The name of the set is not valid** (`BLOCK.VALUE_SET_NAME_INVALID`): use a name that is not empty and has no leading or trailing whitespace. - **The set does not exist** (`BLOCK.VALUE_SET_NOT_FOUND`): the set was destroyed, or the scene was replaced. Find the set again with `findValueSet(byName:)`. - **A set value without a variable** (`BLOCK.VARIABLE_NOT_FOUND`): create the scene variable first. - **The value has another type** (`BLOCK.VARIABLE_TYPE_MISMATCH`): use the type of the scene variable of the same key. - **No scene exists** (`SCENE.MUST_EXIST`): create or load a scene before you create, change or read a value set. Without a scene, `findAllValueSets()` returns an empty list, and `findValueSet(byName:)` and `getActiveValueSet()` return `nil`. The [Error Catalog](https://img.ly/docs/cesdk/mac-catalyst/concepts/error-catalog-z3djzn/) lists every error code. ## API Reference | Method | Description | | --- | --- | | `engine.variable.set(key:value:target:)` and the other setters | Write the shown layer, the default (`.defaultValue`) or a set value (`.valueSet(id)`) | | `engine.variable.get(key:target:)` and the other getters | Read the shown value, or one layer | | `engine.variable.remove(key:target:)` | Remove a variable, or the value of one set | | `engine.variable.createValueSet(name:)` | Create a value set | | `engine.variable.destroyValueSet(_:)` | Destroy a value set and its values | | `engine.variable.setValueSetName(_:name:)` / `getValueSetName(_:)` | Rename a set, or read its name | | `engine.variable.findAllValueSets()` | List the sets of the scene | | `engine.variable.findValueSet(byName:)` | Find a set by its name, or `nil` | | `engine.variable.setActiveValueSet(_:)` / `getActiveValueSet()` | Activate a set or `nil`, or read the active set | ## Next Steps - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/): fill `{{key}}` tokens in text with values. - [Variable Bindings](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/variable-bindings-faa12f/): drive block properties from typed variables. - [Undo and History](https://img.ly/docs/cesdk/mac-catalyst/concepts/undo-and-history-99479d/): work with undo steps. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Variable Bindings" description: "Drive block colors, sizes, images and typefaces from typed variables, so one change updates every bound property." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/variable-bindings-faa12f/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Dynamic Content](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content-53fad7/) > [Variable Bindings](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/variable-bindings-faa12f/) --- ```swift file=@cesdk_swift_examples/engine-guides-variable-bindings/VariableBindings.swift reference-only import Foundation import IMGLYEngine struct VariableBindingsResult { let accentType: VariableType let cornerRadius: Float let colorVariables: [String] let textVariables: [String] let updatedCardColor: Color let cardFillBinding: String? let opacityBinding: String? let shapeBindings: [String] let boundWriteErrorCode: EngineErrorCode? let detachedCornerRadius: Float let boundCornerRadius: Float let renamedBinding: String? let renamedText: String let renameErrorCodes: [EngineErrorCode?] let typeMismatchErrorCode: EngineErrorCode? let bindingTypeErrorCode: EngineErrorCode? } @MainActor func variableBindings(engine: Engine) async throws -> VariableBindingsResult { // Resolve sample assets (font, images) against the engine's configured base URL. let baseURL = try engine.guidesBaseURL let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) // One variable per type. A setter creates the variable when the key is new. let roboto = Typeface( name: "Roboto", fonts: [ Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), ], ) try engine.variable.set(key: "accent", color: .rgba(r: 0.48, g: 0.23, b: 0.93, a: 1)) try engine.variable.set(key: "cornerRadius", number: 40) try engine.variable.set( key: "heroImage", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.variable.set(key: "headline", typeface: roboto) try engine.variable.set(key: "title", value: "Summer Sale") let accentType = try engine.variable.getType(key: "accent") // .color let cornerRadius = try engine.variable.getNumber(key: "cornerRadius") // 40 let colorVariables = engine.variable.findAll(type: .color) // ["accent"] // findAll() lists text variables only let textVariables = engine.variable.findAll() // ["title"] print(accentType, cornerRadius, colorVariables, textVariables) // A card with a color fill and a rect shape let card = try engine.block.create(.graphic) try engine.block.setShape(card, shape: engine.block.createShape(.rect)) try engine.block.setFill(card, fill: engine.block.createFill(.color)) try engine.block.setWidth(card, value: 720) try engine.block.setHeight(card, value: 520) try engine.block.setPositionX(card, value: 40) try engine.block.setPositionY(card, value: 40) try engine.block.appendChild(to: page, child: card) let cardFill = try engine.block.getFill(card) let cardShape = try engine.block.getShape(card) // An image with an image fill let image = try engine.block.create(.graphic) try engine.block.setShape(image, shape: engine.block.createShape(.rect)) let imageFill = try engine.block.createFill(.image) try engine.block.setFill(image, fill: imageFill) try engine.block.setWidth(image, value: 640) try engine.block.setHeight(image, value: 300) try engine.block.setPositionX(image, value: 80) try engine.block.setPositionY(image, value: 80) try engine.block.appendChild(to: page, child: image) // A headline that shows the text variable let text = try engine.block.create(.text) try engine.block.replaceText(text, text: "{{title}}") try engine.block.setWidth(text, value: 640) try engine.block.setHeightMode(text, mode: .auto) try engine.block.setPositionX(text, value: 80) try engine.block.setPositionY(text, value: 420) try engine.block.appendChild(to: page, child: text) // Each binding applies the current value of the variable at once try engine.block.setVariableBinding(cardFill, property: "fill/color/value", key: "accent") for corner in ["TL", "TR", "BL", "BR"] { try engine.block.setVariableBinding(cardShape, property: "shape/rect/cornerRadius\(corner)", key: "cornerRadius") } try engine.block.setVariableBinding(imageFill, property: "fill/image/imageFileURI", key: "heroImage") try engine.block.setVariableBinding(text, property: "text/typeface", key: "headline") // Every bound property follows the new values try engine.variable.set(key: "accent", color: .rgba(r: 0.05, g: 0.55, b: 0.55, a: 1)) try engine.variable.set(key: "cornerRadius", number: 24) try engine.variable.set( key: "heroImage", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) let updatedCardColor: Color = try engine.block.getColor(cardFill, property: "fill/color/value") let cardFillBinding = try engine.block.getVariableBinding(cardFill, property: "fill/color/value") // "accent" let opacityBinding = try engine.block.getVariableBinding(card, property: "opacity") // nil // The four corner radius properties of the shape let shapeBindings = try engine.block.findAllVariableBindings(cardShape) // A bound property changes only through its variable var boundWriteErrorCode: EngineErrorCode? do { try engine.block.setColor(cardFill, property: "fill/color/value", color: .rgba(r: 1, g: 0, b: 0, a: 1)) } catch let error as EngineError { boundWriteErrorCode = error.catalogCode // .blockPropertyBound } // The bottom corners keep their current radius and stop following try engine.block.removeVariableBinding(cardShape, property: "shape/rect/cornerRadiusBL") try engine.block.removeVariableBinding(cardShape, property: "shape/rect/cornerRadiusBR") try engine.variable.set(key: "cornerRadius", number: 60) let detachedCornerRadius = try engine.block.getFloat(cardShape, property: "shape/rect/cornerRadiusBL") // 24 let boundCornerRadius = try engine.block.getFloat(cardShape, property: "shape/rect/cornerRadiusTL") // 60 // Bindings and {{tokens}} in text follow the new name try engine.variable.rename(key: "accent", to: "brand/accent") try engine.variable.rename(key: "title", to: "campaign/title") let renamedBinding = try engine.block.getVariableBinding(cardFill, property: "fill/color/value") // "brand/accent" let renamedText = try engine.block.getString(text, property: "text/text") // "{{campaign/title}}" // .blockVariableNameInvalid for the first three names, // .blockVariableNameTaken for the name of another variable var renameErrorCodes: [EngineErrorCode?] = [] for name in ["", " padded", "a{{b", "campaign/title"] { do { try engine.variable.rename(key: "brand/accent", to: name) } catch let error as EngineError { renameErrorCodes.append(error.catalogCode) } } var typeMismatchErrorCode: EngineErrorCode? do { // A color variable has no number value _ = try engine.variable.getNumber(key: "brand/accent") } catch let error as EngineError { typeMismatchErrorCode = error.catalogCode // .blockVariableTypeMismatch } var bindingTypeErrorCode: EngineErrorCode? do { // Opacity is a number property, so it cannot bind to a color variable try engine.block.setVariableBinding(card, property: "opacity", key: "brand/accent") } catch let error as EngineError { bindingTypeErrorCode = error.catalogCode // .blockVariableBindingUnsupported } let png = try await engine.block.export(page, mimeType: .png) print("Exported \(png.count) bytes") return VariableBindingsResult( accentType: accentType, cornerRadius: cornerRadius, colorVariables: colorVariables, textVariables: textVariables, updatedCardColor: updatedCardColor, cardFillBinding: cardFillBinding, opacityBinding: opacityBinding, shapeBindings: shapeBindings, boundWriteErrorCode: boundWriteErrorCode, detachedCornerRadius: detachedCornerRadius, boundCornerRadius: boundCornerRadius, renamedBinding: renamedBinding, renamedText: renamedText, renameErrorCodes: renameErrorCodes, typeMismatchErrorCode: typeMismatchErrorCode, bindingTypeErrorCode: bindingTypeErrorCode, ) } ``` Drive block colors, sizes, images and typefaces from typed variables, so one change updates every bound property. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-variable-bindings) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Text variables fill `{{key}}` tokens in text, as the [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) guide shows. Color, number, image and typeface variables hold one typed value each. When you bind a block property to such a variable, the property takes the variable's value and follows every change of it. One brand color can then drive every fill, border and text that uses it. This guide covers the typed variables, binding and unbinding properties, the lock on bound properties, renaming variables, and the errors to expect. ## Creating Typed Variables Each type has its own setter. `set(key:color:)`, `set(key:number:)`, `set(key:imageURL:)` and `set(key:typeface:)` create the variable when the key is new, and update it otherwise. `set(key:value:)` stays the setter for text variables. ```swift highlight-variableBindings-createVariables // One variable per type. A setter creates the variable when the key is new. let roboto = Typeface( name: "Roboto", fonts: [ Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), ], ) try engine.variable.set(key: "accent", color: .rgba(r: 0.48, g: 0.23, b: 0.93, a: 1)) try engine.variable.set(key: "cornerRadius", number: 40) try engine.variable.set( key: "heroImage", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.variable.set(key: "headline", typeface: roboto) try engine.variable.set(key: "title", value: "Summer Sale") ``` A number must be finite. A typeface needs a name and at least one font. One key holds one type, so `set(key: "accent", number: 1)` throws while `accent` is a color variable. ## Reading Variables `getColor(key:)`, `getNumber(key:)`, `getImageURL(key:)` and `getTypeface(key:)` return the values. `getType(key:)` returns a `VariableType`: `.string`, `.color`, `.number`, `.image` or `.typeface`. `findAll(type:)` lists the keys of one type. ```swift highlight-variableBindings-readTypes let accentType = try engine.variable.getType(key: "accent") // .color let cornerRadius = try engine.variable.getNumber(key: "cornerRadius") // 40 let colorVariables = engine.variable.findAll(type: .color) // ["accent"] // findAll() lists text variables only let textVariables = engine.variable.findAll() // ["title"] print(accentType, cornerRadius, colorVariables, textVariables) ``` `findAll()` lists text variables only, as before. Use `findAll(type:)` for the other types. ## Binding Properties We create a card with a color fill and a rect shape, an image with an image fill, and a text that shows the `title` variable. A binding targets the block that owns the property: the fill owns `fill/color/value`, and the shape owns the corner radii. ```swift highlight-variableBindings-createBlocks // A card with a color fill and a rect shape let card = try engine.block.create(.graphic) try engine.block.setShape(card, shape: engine.block.createShape(.rect)) try engine.block.setFill(card, fill: engine.block.createFill(.color)) try engine.block.setWidth(card, value: 720) try engine.block.setHeight(card, value: 520) try engine.block.setPositionX(card, value: 40) try engine.block.setPositionY(card, value: 40) try engine.block.appendChild(to: page, child: card) let cardFill = try engine.block.getFill(card) let cardShape = try engine.block.getShape(card) // An image with an image fill let image = try engine.block.create(.graphic) try engine.block.setShape(image, shape: engine.block.createShape(.rect)) let imageFill = try engine.block.createFill(.image) try engine.block.setFill(image, fill: imageFill) try engine.block.setWidth(image, value: 640) try engine.block.setHeight(image, value: 300) try engine.block.setPositionX(image, value: 80) try engine.block.setPositionY(image, value: 80) try engine.block.appendChild(to: page, child: image) // A headline that shows the text variable let text = try engine.block.create(.text) try engine.block.replaceText(text, text: "{{title}}") try engine.block.setWidth(text, value: 640) try engine.block.setHeightMode(text, mode: .auto) try engine.block.setPositionX(text, value: 80) try engine.block.setPositionY(text, value: 420) try engine.block.appendChild(to: page, child: text) ``` `setVariableBinding(_:property:key:)` binds a property to a variable. The property takes the variable's current value at once. ```swift highlight-variableBindings-bindProperties // Each binding applies the current value of the variable at once try engine.block.setVariableBinding(cardFill, property: "fill/color/value", key: "accent") for corner in ["TL", "TR", "BL", "BR"] { try engine.block.setVariableBinding(cardShape, property: "shape/rect/cornerRadius\(corner)", key: "cornerRadius") } try engine.block.setVariableBinding(imageFill, property: "fill/image/imageFileURI", key: "heroImage") try engine.block.setVariableBinding(text, property: "text/typeface", key: "headline") ``` Each variable type binds to a fixed set of properties: | Variable type | Properties it binds to | | --- | --- | | Color | Color properties, for example `fill/color/value` and `fill/solid/color` | | Number | Float properties, for example `opacity`, `width` and `shape/rect/cornerRadiusTL`. Int properties, such as `shape/star/points`, do not bind | | Image | `fill/image/imageFileURI`, `fill/image/previewFileURI` and `fill/video/fileURI` | | Typeface | `text/typeface` and `caption/typeface` | Text variables and gradient color stops cannot bind. On a text block, a bound typeface, text color or font size applies to the whole text. ## Changing Variables Set a variable again to update every property that is bound to it. ```swift highlight-variableBindings-updateVariables // Every bound property follows the new values try engine.variable.set(key: "accent", color: .rgba(r: 0.05, g: 0.55, b: 0.55, a: 1)) try engine.variable.set(key: "cornerRadius", number: 24) try engine.variable.set( key: "heroImage", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) let updatedCardColor: Color = try engine.block.getColor(cardFill, property: "fill/color/value") ``` An image variable also empties the source set of the fill, so the fill shows the URL of the variable. When one bound property rejects a value, for example an opacity above 1, the setter throws the first error. The variable and the other bound properties change anyway. ## Reading Bindings `getVariableBinding(_:property:)` returns the key of the variable that a property is bound to, or `nil`. `findAllVariableBindings(_:)` lists the bound properties of a block. ```swift highlight-variableBindings-readBindings let cardFillBinding = try engine.block.getVariableBinding(cardFill, property: "fill/color/value") // "accent" let opacityBinding = try engine.block.getVariableBinding(card, property: "opacity") // nil // The four corner radius properties of the shape let shapeBindings = try engine.block.findAllVariableBindings(cardShape) ``` ## Bound Properties Are Locked A bound property changes only through its variable. A direct write throws an `EngineError` whose `catalogCode` is `.blockPropertyBound`, and the value stays as it is. ```swift highlight-variableBindings-boundPropertyLocked // A bound property changes only through its variable var boundWriteErrorCode: EngineErrorCode? do { try engine.block.setColor(cardFill, property: "fill/color/value", color: .rgba(r: 1, g: 0, b: 0, a: 1)) } catch let error as EngineError { boundWriteErrorCode = error.catalogCode // .blockPropertyBound } ``` Moving, resizing, rotating, cropping, aligning and scaling also leave a bound property unchanged. A bound typeface also locks `setTypeface` and `setFont`. Keyframes, undo and loading a scene still change a bound property. Changing the design unit also converts a bound length, such as a width or a corner radius. A number variable whose bound properties all hold its value converts with them. Otherwise the variable keeps its value, and the next write to the variable sets the lengths in the new unit. ## Removing Bindings `removeVariableBinding(_:property:)` removes the binding. The property keeps its current value and no longer follows the variable. ```swift highlight-variableBindings-removeBinding // The bottom corners keep their current radius and stop following try engine.block.removeVariableBinding(cardShape, property: "shape/rect/cornerRadiusBL") try engine.block.removeVariableBinding(cardShape, property: "shape/rect/cornerRadiusBR") try engine.variable.set(key: "cornerRadius", number: 60) let detachedCornerRadius = try engine.block.getFloat(cardShape, property: "shape/rect/cornerRadiusBL") // 24 let boundCornerRadius = try engine.block.getFloat(cardShape, property: "shape/rect/cornerRadiusTL") // 60 ``` `remove(key:)` removes a variable of any type. Its bound properties keep the last value and the binding. When you set a variable with the same key and type again, they follow it again. ## Renaming Variables `rename(key:to:)` moves the value and the type of a variable to a new key. Every binding follows the new key. For a text variable, every `{{key}}` token in text changes to the new key. ```swift highlight-variableBindings-rename // Bindings and {{tokens}} in text follow the new name try engine.variable.rename(key: "accent", to: "brand/accent") try engine.variable.rename(key: "title", to: "campaign/title") let renamedBinding = try engine.block.getVariableBinding(cardFill, property: "fill/color/value") // "brand/accent" let renamedText = try engine.block.getString(text, property: "text/text") // "{{campaign/title}}" ``` A new name must follow these rules: - It is not empty. - It does not start or end with whitespace. Spaces inside the name are allowed. - It does not contain `{{` or `}}`, because text tokens use them. - No other variable uses it. A rename to the current name does nothing. ```swift highlight-variableBindings-renameErrors // .blockVariableNameInvalid for the first three names, // .blockVariableNameTaken for the name of another variable var renameErrorCodes: [EngineErrorCode?] = [] for name in ["", " padded", "a{{b", "campaign/title"] { do { try engine.variable.rename(key: "brand/accent", to: name) } catch let error as EngineError { renameErrorCodes.append(error.catalogCode) } } ``` ## Handling Type Errors A getter of another type and a binding to a property of another type throw errors. The variable and the property stay unchanged. ```swift highlight-variableBindings-typeErrors var typeMismatchErrorCode: EngineErrorCode? do { // A color variable has no number value _ = try engine.variable.getNumber(key: "brand/accent") } catch let error as EngineError { typeMismatchErrorCode = error.catalogCode // .blockVariableTypeMismatch } var bindingTypeErrorCode: EngineErrorCode? do { // Opacity is a number property, so it cannot bind to a color variable try engine.block.setVariableBinding(card, property: "opacity", key: "brand/accent") } catch let error as EngineError { bindingTypeErrorCode = error.catalogCode // .blockVariableBindingUnsupported } ``` ## Saving and Undo A scene or an archive saves the typed variables and the bindings. Undo and redo restore variable values, bindings and renames. ## Exporting the Result We export the page as a PNG. The card shows the last values of its variables. ```swift highlight-variableBindings-export let png = try await engine.block.export(page, mimeType: .png) print("Exported \(png.count) bytes") ``` ## Troubleshooting - **The variable does not exist** (`.blockVariableNotFound`): set the variable before you read or bind it. - **The key holds another type** (`.blockVariableTypeMismatch`): use the getter and setter of the stored type, or remove the variable first. - **The typeface is not accepted** (`.blockVariableTypefaceInvalid`): pass a typeface with a name and at least one font. - **The property cannot bind to this variable** (`.blockVariableBindingUnsupported`): check the table in Binding Properties, and pass the block that owns the property. - **Gradient stops do not bind** (`.blockVariableBindingGradientStopsUnsupported`): bind a color variable to a solid color fill instead. - **A write to a property fails** (`.blockPropertyBound`): set the variable instead, or remove the binding first. - **A rename fails with an invalid name** (`.blockVariableNameInvalid`): follow the name rules in Renaming Variables. - **A rename fails with a taken name** (`.blockVariableNameTaken`): pick a name that no variable uses. The [Error Catalog](https://img.ly/docs/cesdk/mac-catalyst/concepts/error-catalog-z3djzn/) lists every error code. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.variable.set(key:color:)` / `getColor(key:)` | Set or read a color variable | | `engine.variable.set(key:number:)` / `getNumber(key:)` | Set or read a number variable | | `engine.variable.set(key:imageURL:)` / `getImageURL(key:)` | Set or read an image variable | | `engine.variable.set(key:typeface:)` / `getTypeface(key:)` | Set or read a typeface variable | | `engine.variable.set(key:value:)` | Set a text variable | | `engine.variable.getType(key:)` | Get the type of a variable | | `engine.variable.findAll(type:)` | List the keys of the variables of one type | | `engine.variable.findAll()` | List the keys of the text variables | | `engine.variable.rename(key:to:)` | Rename a variable of any type | | `engine.variable.remove(key:)` | Remove a variable of any type | | `engine.block.setVariableBinding(_:property:key:)` | Bind a property to a variable | | `engine.block.getVariableBinding(_:property:)` | Get the key of the bound variable, or `nil` | | `engine.block.findAllVariableBindings(_:)` | List the bound properties of a block | | `engine.block.removeVariableBinding(_:property:)` | Remove a binding and keep the current value | ## Next Steps - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/): fill `{{key}}` tokens in text with values. - [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/): mark the blocks that template users can replace. - [Undo and History](https://img.ly/docs/cesdk/mac-catalyst/concepts/undo-and-history-99479d/): work with undo steps. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Add to Template Library" description: "Save and organize templates in an asset source for users to browse and apply from the template library." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-to-template-library-8bfbc7/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Add to Template Library](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-to-template-library-8bfbc7/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-templates-add-to-template-library/AddToTemplateLibrary.swift reference-only import Foundation import IMGLYEngine @MainActor func addToTemplateLibrary(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL // Load a design so there is something to save as a template. In your app this // is whatever the user is currently editing. let starterURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: starterURL) let templateString = try await engine.scene.saveToString() let stringURL = FileManager.default.temporaryDirectory .appendingPathComponent("template-\(UUID().uuidString).imgly") try templateString.write(to: stringURL, atomically: true, encoding: .utf8) let templateArchive = try await engine.scene.saveToArchive() let archiveURL = FileManager.default.temporaryDirectory .appendingPathComponent("template-\(UUID().uuidString).imgly") try templateArchive.write(to: archiveURL) try engine.asset.addLocalSource(sourceID: "my-templates", applyAsset: { [weak engine] asset in guard let engine, let uri = asset.meta?["uri"], let url = URL(string: uri) else { return nil } try await engine.scene.applyTemplate(from: url) return nil }) let templates = [ AssetDefinition( id: "template-business-card", meta: [ "uri": baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene").absoluteString, "thumbUri": baseURL.appendingPathComponent("ly.img.templates/thumbnails/cesdk_business_card_1.jpg") .absoluteString, ], label: ["en": "Business Card"], ), AssetDefinition( id: "template-blank", meta: [ "uri": baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_blank_1.scene").absoluteString, "thumbUri": baseURL.appendingPathComponent("ly.img.templates/thumbnails/cesdk_blank_1.png").absoluteString, ], label: ["en": "Blank"], ), AssetDefinition( id: "template-postcard", meta: [ "uri": baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_postcard_1.scene").absoluteString, "thumbUri": baseURL.appendingPathComponent("ly.img.templates/thumbnails/cesdk_postcard_1.jpg").absoluteString, ], label: ["en": "Postcard"], ), ] for template in templates { try engine.asset.addAsset(to: "my-templates", asset: template) } let sources = engine.asset.findAllSources() print("Registered sources:", sources) let results = try await engine.asset.findAssets( sourceID: "my-templates", query: .init(query: nil, page: 0, perPage: 10), ) print("Templates in library:", results.total) try engine.asset.removeAsset(from: "my-templates", assetID: "template-postcard") try engine.asset.assetSourceContentsChanged(sourceID: "my-templates") } ``` Create a template library where templates are stored, managed, and applied programmatically through a custom asset source. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-templates-add-to-template-library) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Templates in CE.SDK are stored and accessed through the asset system. A template library is a local asset source configured to hold and serve template assets, so users can browse thumbnails and apply templates to their designs. This guide covers saving scenes as templates, creating a template asset source, adding templates with metadata, and managing them. ## Saving Templates With a design open in the engine, export the current scene as a template file. Scenes serialize in two formats. ### String Format Use `engine.scene.saveToString()` to serialize the scene to a base64 string. This lightweight format references assets by URL, so it is ideal for templates whose assets are hosted on a CDN. Persist the string wherever you store templates — a database, or a `.scene` file as shown here. ```swift highlight-addToTemplateLibrary-saveString let templateString = try await engine.scene.saveToString() let stringURL = FileManager.default.temporaryDirectory .appendingPathComponent("template-\(UUID().uuidString).imgly") try templateString.write(to: stringURL, atomically: true, encoding: .utf8) ``` ### Archive Format For self-contained templates that bundle all assets, use `engine.scene.saveToArchive()`. It returns `Data` (the `Blob` typealias) holding a `.zip` archive with every asset embedded, making the template portable without external dependencies. ```swift highlight-addToTemplateLibrary-saveArchive let templateArchive = try await engine.scene.saveToArchive() let archiveURL = FileManager.default.temporaryDirectory .appendingPathComponent("template-\(UUID().uuidString).imgly") try templateArchive.write(to: archiveURL) ``` ## Creating a Template Asset Source Register a local asset source with `engine.asset.addLocalSource(sourceID:applyAsset:)`. The `applyAsset` closure runs when a template is selected: it reads the asset's `meta["uri"]` and calls `engine.scene.applyTemplate(from:)`, which keeps the current scene's page dimensions and design unit while resizing the template's content to fit them. Return `nil` because applying a template mutates the current scene rather than creating a new block. Capture `engine` weakly: the source retains this callback for its lifetime, so a strong capture would risk a retain cycle. ```swift highlight-addToTemplateLibrary-createSource try engine.asset.addLocalSource(sourceID: "my-templates", applyAsset: { [weak engine] asset in guard let engine, let uri = asset.meta?["uri"], let url = URL(string: uri) else { return nil } try await engine.scene.applyTemplate(from: url) return nil }) ``` `applyTemplate(from:)` accepts a serialized scene string or a URL to a `.scene` file — not a `.zip` archive. To start from an archive, load it with `engine.scene.load(from:)`, which replaces the current scene instead of merging a template into it. ## Adding Templates to the Source Register templates with `engine.asset.addAsset(to:asset:)`, passing an `AssetDefinition` that carries the display metadata and load URLs. Build each `uri` and `thumbUri` from the base URL where you host your assets. ```swift highlight-addToTemplateLibrary-addTemplates let templates = [ AssetDefinition( id: "template-business-card", meta: [ "uri": baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene").absoluteString, "thumbUri": baseURL.appendingPathComponent("ly.img.templates/thumbnails/cesdk_business_card_1.jpg") .absoluteString, ], label: ["en": "Business Card"], ), AssetDefinition( id: "template-blank", meta: [ "uri": baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_blank_1.scene").absoluteString, "thumbUri": baseURL.appendingPathComponent("ly.img.templates/thumbnails/cesdk_blank_1.png").absoluteString, ], label: ["en": "Blank"], ), AssetDefinition( id: "template-postcard", meta: [ "uri": baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_postcard_1.scene").absoluteString, "thumbUri": baseURL.appendingPathComponent("ly.img.templates/thumbnails/cesdk_postcard_1.jpg").absoluteString, ], label: ["en": "Postcard"], ), ] for template in templates { try engine.asset.addAsset(to: "my-templates", asset: template) } ``` Each template asset requires: - `id` — Unique identifier for the template - `label` — Localized display name shown in the template library. `IMGLYEngine.Locale` is a `String` typealias, so use plain language keys such as `["en": "Business Card"]` - `meta["uri"]` — URL to the `.scene` file or archive loaded when the template is applied - `meta["thumbUri"]` — URL to a preview image displayed in the template library grid ## Managing Templates After the initial setup, manage templates programmatically. ```swift highlight-addToTemplateLibrary-manage let sources = engine.asset.findAllSources() print("Registered sources:", sources) let results = try await engine.asset.findAssets( sourceID: "my-templates", query: .init(query: nil, page: 0, perPage: 10), ) print("Templates in library:", results.total) try engine.asset.removeAsset(from: "my-templates", assetID: "template-postcard") try engine.asset.assetSourceContentsChanged(sourceID: "my-templates") ``` List registered source IDs with `engine.asset.findAllSources()`, and page through a source's templates with `engine.asset.findAssets(sourceID:query:)` — `results.total` reports the count. Remove a template with `engine.asset.removeAsset(from:assetID:)`, then call `engine.asset.assetSourceContentsChanged(sourceID:)` to notify any connected UI that the source changed. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Template fails to load | Incorrect URI in the asset's `meta` | Verify `meta["uri"]` points to a valid `.scene` file or archive | | Apply callback not triggered | `applyAsset` closure not provided to `addLocalSource` | Pass the `applyAsset:` closure when creating the source | | Empty query results | Templates queried before they were added | Call `addAsset(to:asset:)` before `findAssets(sourceID:query:)` | ## API Reference | Method | Description | | --- | --- | | `engine.asset.addLocalSource(sourceID:applyAsset:)` | Register a local asset source with an apply callback | | `engine.asset.addAsset(to:asset:)` | Add an asset to a registered source | | `engine.asset.removeAsset(from:assetID:)` | Remove an asset from a source by ID | | `engine.asset.findAllSources()` | List registered asset source IDs | | `engine.asset.findAssets(sourceID:query:)` | Query a source's assets with paging | | `engine.asset.assetSourceContentsChanged(sourceID:)` | Notify the UI that a source's contents changed | | `engine.scene.saveToString()` | Serialize the scene to a base64 string | | `engine.scene.saveToArchive()` | Save the scene as a self-contained archive (`Data`) | | `engine.scene.applyTemplate(from:)` | Apply a template to the current scene | ## Next Steps - [Create From Scratch](https://img.ly/docs/cesdk/mac-catalyst/create-templates/from-scratch-663cda/) — Build reusable templates programmatically with the Engine API - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) — Add dynamic text content to templates - [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) — Create editable image and video areas - [Customize Asset Library](#broken-link-c9a4de) — Organize and present asset sources in the library --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Edit or Remove Templates" description: "Add, edit, remove, and update design templates in a local asset source with the CE.SDK engine." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/edit-or-remove-38a8be/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Edit or Remove Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates/edit-or-remove-38a8be/) --- ```swift file=@cesdk_swift_examples/engine-guides-edit-or-remove-templates/EditOrRemoveTemplates.swift reference-only import Foundation import IMGLYEngine @MainActor func editOrRemoveTemplates(engine: Engine) async throws { // Demo scaffolding: a scene with one page that serves as the template content. // Passing the design unit to `create` also pairs the font-size unit to pixels, // so the `text/fontSize` values below are interpreted as pixels — the default // font-size unit is points, which the scene's DPI would otherwise scale up. let scene = try engine.scene.create(designUnit: .px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) try engine.asset.addLocalSource(sourceID: "my-templates", applyAsset: { [weak engine] asset in guard let engine, let uri = asset.meta?["uri"], let base64Content = uri.split(separator: ",", maxSplits: 1).dropFirst().first else { return nil } try await engine.scene.load(from: String(base64Content)) return nil }) let titleBlock = try engine.block.create(.text) try engine.block.replaceText(titleBlock, text: "Original Template") try engine.block.setFloat(titleBlock, property: "text/fontSize", value: 64) try engine.block.setWidthMode(titleBlock, mode: .auto) try engine.block.setHeightMode(titleBlock, mode: .auto) try engine.block.appendChild(to: page, child: titleBlock) let subtitleBlock = try engine.block.create(.text) try engine.block.replaceText(subtitleBlock, text: "A reusable starting point") try engine.block.setFloat(subtitleBlock, property: "text/fontSize", value: 42) try engine.block.setWidthMode(subtitleBlock, mode: .auto) try engine.block.setHeightMode(subtitleBlock, mode: .auto) try engine.block.appendChild(to: page, child: subtitleBlock) // Position the text blocks centered on the page. let titleWidth = try engine.block.getFrameWidth(titleBlock) let titleHeight = try engine.block.getFrameHeight(titleBlock) try engine.block.setPositionX(titleBlock, value: (pageWidth - titleWidth) / 2) try engine.block.setPositionY(titleBlock, value: pageHeight / 2 - titleHeight - 20) let subtitleWidth = try engine.block.getFrameWidth(subtitleBlock) try engine.block.setPositionX(subtitleBlock, value: (pageWidth - subtitleWidth) / 2) try engine.block.setPositionY(subtitleBlock, value: pageHeight / 2 + 20) // Capture the composed template for the guide's hero image (verification only — // not part of the rendered snippets). try await engine.captureGuide(page, label: "hero", mimeType: .png) let originalContent = try await engine.scene.saveToString() try engine.asset.addAsset(to: "my-templates", asset: AssetDefinition( id: "template-original", meta: [ "uri": "data:application/octet-stream;base64,\(originalContent)", "thumbUri": try await templateThumbnail(engine: engine, page: page), ], label: ["en": "Original Template"], )) try engine.block.replaceText(titleBlock, text: "Updated Template") try engine.block.replaceText(subtitleBlock, text: "This template was edited and saved") let updatedContent = try await engine.scene.saveToString() try engine.asset.addAsset(to: "my-templates", asset: AssetDefinition( id: "template-updated", meta: [ "uri": "data:application/octet-stream;base64,\(updatedContent)", "thumbUri": try await templateThumbnail(engine: engine, page: page), ], label: ["en": "Updated Template"], )) // Re-center the text blocks after the edits changed their frame sizes. let newTitleWidth = try engine.block.getFrameWidth(titleBlock) let newTitleHeight = try engine.block.getFrameHeight(titleBlock) try engine.block.setPositionX(titleBlock, value: (pageWidth - newTitleWidth) / 2) try engine.block.setPositionY(titleBlock, value: pageHeight / 2 - newTitleHeight - 20) let newSubtitleWidth = try engine.block.getFrameWidth(subtitleBlock) try engine.block.setPositionX(subtitleBlock, value: (pageWidth - newSubtitleWidth) / 2) // Add a temporary template to demonstrate removal. try engine.asset.addAsset(to: "my-templates", asset: AssetDefinition( id: "template-temporary", meta: [ "uri": "data:application/octet-stream;base64,\(originalContent)", "thumbUri": try await templateThumbnail(engine: engine, page: page), ], label: ["en": "Temporary Template"], )) try engine.asset.removeAsset(from: "my-templates", assetID: "template-temporary") try engine.block.replaceText(subtitleBlock, text: "Updated again with new content") let reUpdatedContent = try await engine.scene.saveToString() try engine.asset.removeAsset(from: "my-templates", assetID: "template-updated") try engine.asset.addAsset(to: "my-templates", asset: AssetDefinition( id: "template-updated", meta: [ "uri": "data:application/octet-stream;base64,\(reUpdatedContent)", "thumbUri": try await templateThumbnail(engine: engine, page: page), ], label: ["en": "Updated Template"], )) // Restore the original template content as the active scene. try await engine.scene.load(from: originalContent) } private func templateThumbnail(engine: Engine, page: DesignBlockID) async throws -> String { // `targetWidth` bounds the export so the thumbnail stays small. let data = try await engine.block.export(page, mimeType: .png, options: ExportOptions(targetWidth: 200)) return "data:image/png;base64,\(data.base64EncodedString())" } ``` Modify existing templates and manage template lifecycle in your asset library using CE.SDK. ![A template page with the centered heading "Original Template" above the subtitle "A reusable starting point".](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-edit-or-remove-templates) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Templates evolve as designs change. You might need to update branding, fix content errors, or remove outdated templates from your library. CE.SDK provides APIs for adding, editing, and removing templates from asset sources. This guide covers how to add templates to asset sources, edit template content, remove templates, and save updated versions. ## Adding Templates First, create a local asset source to store your templates. The `applyAsset` callback runs when a template from this source is applied to the scene — it reads the base64 scene data from the asset's `uri` metadata entry and loads it with `engine.scene.load(from:)`. Return `nil` because applying a template replaces the current scene content rather than creating a new block. The source retains this callback for its lifetime, so capture the engine weakly to avoid risking a retain cycle. ```swift highlight-editOrRemoveTemplates-createSource try engine.asset.addLocalSource(sourceID: "my-templates", applyAsset: { [weak engine] asset in guard let engine, let uri = asset.meta?["uri"], let base64Content = uri.split(separator: ",", maxSplits: 1).dropFirst().first else { return nil } try await engine.scene.load(from: String(base64Content)) return nil }) ``` Next, create your template content using block APIs: ```swift highlight-editOrRemoveTemplates-createTemplate let titleBlock = try engine.block.create(.text) try engine.block.replaceText(titleBlock, text: "Original Template") try engine.block.setFloat(titleBlock, property: "text/fontSize", value: 64) try engine.block.setWidthMode(titleBlock, mode: .auto) try engine.block.setHeightMode(titleBlock, mode: .auto) try engine.block.appendChild(to: page, child: titleBlock) let subtitleBlock = try engine.block.create(.text) try engine.block.replaceText(subtitleBlock, text: "A reusable starting point") try engine.block.setFloat(subtitleBlock, property: "text/fontSize", value: 42) try engine.block.setWidthMode(subtitleBlock, mode: .auto) try engine.block.setHeightMode(subtitleBlock, mode: .auto) try engine.block.appendChild(to: page, child: subtitleBlock) ``` Then save the template with `saveToString()` and add it to the asset source using `addAsset(to:asset:)`. Each template needs a unique ID, a label, and metadata containing the template URI and thumbnail: ```swift highlight-editOrRemoveTemplates-addToSource let originalContent = try await engine.scene.saveToString() try engine.asset.addAsset(to: "my-templates", asset: AssetDefinition( id: "template-original", meta: [ "uri": "data:application/octet-stream;base64,\(originalContent)", "thumbUri": try await templateThumbnail(engine: engine, page: page), ], label: ["en": "Original Template"], )) ``` The `uri` entry in the `meta` dictionary contains the template content as a data URI. The `thumbUri` entry provides the thumbnail the asset library displays. The example exports the rendered template to a small PNG with `engine.block.export(_:mimeType:options:)`, so the thumbnail previews the actual content: ```swift highlight-editOrRemoveTemplates-thumbnailHelper private func templateThumbnail(engine: Engine, page: DesignBlockID) async throws -> String { // `targetWidth` bounds the export so the thumbnail stays small. let data = try await engine.block.export(page, mimeType: .png, options: ExportOptions(targetWidth: 200)) return "data:image/png;base64,\(data.base64EncodedString())" } ``` > **Note:** Use a raster format such as PNG or JPEG for `thumbUri`. The asset library displays raster thumbnails directly across platforms; vector formats like SVG are not decoded by the native image loaders and render as broken thumbnails. ## Editing Templates Modify template content using block APIs. The example updates the text blocks with `replaceText(_:text:)`; the same pattern applies to any block property, such as image fills or positions. ```swift highlight-editOrRemoveTemplates-modifyTemplate try engine.block.replaceText(titleBlock, text: "Updated Template") try engine.block.replaceText(subtitleBlock, text: "This template was edited and saved") let updatedContent = try await engine.scene.saveToString() try engine.asset.addAsset(to: "my-templates", asset: AssetDefinition( id: "template-updated", meta: [ "uri": "data:application/octet-stream;base64,\(updatedContent)", "thumbUri": try await templateThumbnail(engine: engine, page: page), ], label: ["en": "Updated Template"], )) ``` After editing, save the modified template as a new asset or update an existing one. ## Removing Templates Remove templates from asset sources using `removeAsset(from:assetID:)`. This permanently deletes the template entry from the source. ```swift highlight-editOrRemoveTemplates-removeTemplate // Add a temporary template to demonstrate removal. try engine.asset.addAsset(to: "my-templates", asset: AssetDefinition( id: "template-temporary", meta: [ "uri": "data:application/octet-stream;base64,\(originalContent)", "thumbUri": try await templateThumbnail(engine: engine, page: page), ], label: ["en": "Temporary Template"], )) try engine.asset.removeAsset(from: "my-templates", assetID: "template-temporary") ``` > **Warning:** Removal is permanent. The template is no longer accessible from the asset source after removal. If you need to restore templates, maintain backups or implement a soft-delete mechanism. ## Saving Updated Templates To update an existing template, first remove it using `removeAsset(from:assetID:)`, then add the updated version with `addAsset(to:asset:)` using the same asset ID. Adding an asset whose ID already exists in the source throws an error, so the removal has to come first. ```swift highlight-editOrRemoveTemplates-updateInSource try engine.block.replaceText(subtitleBlock, text: "Updated again with new content") let reUpdatedContent = try await engine.scene.saveToString() try engine.asset.removeAsset(from: "my-templates", assetID: "template-updated") try engine.asset.addAsset(to: "my-templates", asset: AssetDefinition( id: "template-updated", meta: [ "uri": "data:application/octet-stream;base64,\(reUpdatedContent)", "thumbUri": try await templateThumbnail(engine: engine, page: page), ], label: ["en": "Updated Template"], )) ``` Reusing the asset ID keeps existing references to the template valid while the content changes underneath. ## Best Practices ### Versioning Strategies When managing template updates, consider these approaches: - **Replace in place**: Reuse the same asset ID — remove the old entry, then add the update. Existing references to the template keep working. - **Version suffixes**: Create new entries with version identifiers (e.g., `template-v2`). This preserves old versions while introducing new ones. - **Archive old versions**: Move deprecated templates to a separate source before removal. This maintains a history without cluttering the main library. ### Change Notifications Each `addAsset(to:asset:)` and `removeAsset(from:assetID:)` call emits a contents-changed signal for its source, so observers of the source stay up to date without extra work. Call `assetSourceContentsChanged(sourceID:)` explicitly when template data changes through a path the engine can't observe — for example, when the backing store of a custom asset source updates. ### Template IDs Use descriptive, unique IDs that reflect the template's purpose (e.g., `marketing-banner-2024`, `social-post-square`). Consistent naming conventions make templates easier to find and manage programmatically. ### Thumbnails Generate meaningful thumbnails that accurately represent template content. Good thumbnails improve discoverability in the asset library and help users quickly identify the right template. ### Memory Considerations Templates stored as base64 data URIs remain in memory. For production applications with many templates, consider storing template content externally and using URLs in the `uri` metadata entry instead of inline data URIs. ## API Reference | Method | Description | | --- | --- | | `engine.asset.addLocalSource(sourceID:applyAsset:)` | Create a local asset source | | `engine.asset.addAsset(to:asset:)` | Add a template to an asset source | | `engine.asset.removeAsset(from:assetID:)` | Remove a template from an asset source | | `engine.asset.assetSourceContentsChanged(sourceID:)` | Signal observers that a source's contents changed | | `engine.scene.saveToString()` | Save the scene as a base64 string | | `engine.scene.load(from:)` | Load a scene from a base64 string | | `engine.block.export(_:mimeType:options:)` | Render a block to image data (used for the thumbnail) | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create From Scratch" description: "Build reusable design templates programmatically using CE.SDK's APIs. Create scenes, add text and graphic blocks, configure placeholders and variables, apply editing constraints, and save templates for reuse." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/from-scratch-663cda/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Create From Scratch](https://img.ly/docs/cesdk/mac-catalyst/create-templates/from-scratch-663cda/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-templates-from-scratch/CreateTemplateFromScratch.swift reference-only import Foundation import IMGLYEngine @MainActor func createTemplateFromScratch(engine: Engine) async throws { // Resolve sample assets (fonts, image) against the engine's configured base URL. let baseURL = try engine.guidesBaseURL let scene = try engine.scene.create(designUnit: .px, fontSizeUnit: .px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 1000) try engine.block.appendChild(to: scene, child: page) let backgroundFill = try engine.block.createFill(.color) try engine.block.setColor( backgroundFill, property: "fill/color/value", color: .rgba(r: 0.98, g: 0.98, b: 0.99, a: 1), ) try engine.block.setFill(page, fill: backgroundFill) let brandTypeface = Typeface( name: "Brand Sans", fonts: [ Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ), ], ) let brandRegularFont = brandTypeface.fonts[0] let headline = try engine.block.create(.text) try engine.block.replaceText(headline, text: "{{title}}") try engine.block.setFont(headline, fontFileURL: brandRegularFont.uri, typeface: brandTypeface) try engine.block.setTextFontSize(headline, fontSize: 72) try engine.block.setTextColor(headline, color: .rgba(r: 0.09, g: 0.09, b: 0.09, a: 1)) try engine.block.setPositionX(headline, value: 72) try engine.block.setPositionY(headline, value: 96) try engine.block.setWidth(headline, value: 656) try engine.block.setHeightMode(headline, mode: .auto) try engine.block.appendChild(to: page, child: headline) let subtitle = try engine.block.create(.text) try engine.block.replaceText(subtitle, text: "{{subtitle}}") try engine.block.setFont(subtitle, fontFileURL: brandRegularFont.uri, typeface: brandTypeface) try engine.block.setTextFontSize(subtitle, fontSize: 32) try engine.block.setTextColor(subtitle, color: .rgba(r: 0.32, g: 0.32, b: 0.32, a: 1)) try engine.block.setPositionX(subtitle, value: 72) try engine.block.setPositionY(subtitle, value: 194) try engine.block.setWidth(subtitle, value: 620) try engine.block.setHeightMode(subtitle, mode: .auto) try engine.block.appendChild(to: page, child: subtitle) let cta = try engine.block.create(.text) try engine.block.replaceText(cta, text: "{{cta}}") try engine.block.setFont(cta, fontFileURL: brandRegularFont.uri, typeface: brandTypeface) try engine.block.setTextFontSize(cta, fontSize: 36) try engine.block.setTextColor(cta, color: .rgba(r: 0.09, g: 0.09, b: 0.09, a: 1)) try engine.block.setPositionX(cta, value: 72) try engine.block.setPositionY(cta, value: 858) try engine.block.setWidth(cta, value: 400) try engine.block.setHeightMode(cta, mode: .auto) try engine.block.appendChild(to: page, child: cta) try engine.variable.set(key: "title", value: "Summer Sale") try engine.variable.set(key: "subtitle", value: "Up to 50% off all items") try engine.variable.set(key: "cta", value: "Learn More") let variableNames = engine.variable.findAll() print("Template variables:", variableNames) let imageBlock = try engine.block.create(.graphic) try engine.block.setName(imageBlock, name: "hero-image") try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(imageBlock, value: 72) try engine.block.setPositionY(imageBlock, value: 300) try engine.block.setWidth(imageBlock, value: 656) try engine.block.setHeight(imageBlock, value: 500) try engine.block.setContentFillMode(imageBlock, mode: .cover) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.appendChild(to: page, child: imageBlock) let placeholderFill = try engine.block.getFill(imageBlock) if try engine.block.supportsPlaceholderBehavior(placeholderFill) { try engine.block.setPlaceholderBehaviorEnabled(placeholderFill, enabled: true) } try engine.block.setPlaceholderEnabled(imageBlock, enabled: true) if try engine.block.supportsPlaceholderControls(imageBlock) { try engine.block.setPlaceholderControlsOverlayEnabled(imageBlock, enabled: true) try engine.block.setPlaceholderControlsButtonEnabled(imageBlock, enabled: true) } try engine.editor.setGlobalScope(key: "layer/move", value: .defer) try engine.editor.setGlobalScope(key: "layer/resize", value: .defer) try engine.editor.setGlobalScope(key: "fill/change", value: .defer) for block in [headline, subtitle, cta, imageBlock] { try engine.block.setScopeEnabled(block, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(block, key: "layer/resize", enabled: false) } try engine.block.setScopeEnabled(imageBlock, key: "fill/change", enabled: true) let imageCanMove = try engine.block.isAllowedByScope(imageBlock, key: "layer/move") let imageFillCanChange = try engine.block.isAllowedByScope(imageBlock, key: "fill/change") print("Image can move:", imageCanMove, "— fill can change:", imageFillCanChange) // Most-evolved scene — the finished promotional card, promoted to the guide's hero image. try await engine.captureGuide(page, label: "hero") try await engine.block.forceLoadResources([page]) let templateString = try await engine.scene.saveToString() let templateArchive = try await engine.scene.saveToArchive() print("Template string characters:", templateString.count) print("Template archive bytes:", templateArchive.count) } ``` Build reusable design templates entirely through code for automation, batch generation, and custom template creation tools. ![A promotional card template with the "Summer Sale" headline above the "Up to 50% off all items" subtitle, a swappable hero image, and a "Learn More" call to action.](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-templates-from-scratch) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. CE.SDK lets you create a template without starting from an existing scene. You define the page size, add text and graphic blocks, bind text variables, mark media as a placeholder, restrict editing with scopes, and save the result for reuse. This guide builds a promotional card template with variable-driven text and one swappable image area. The sample assumes an existing `Engine` instance and focuses on the scene and block APIs that assemble the reusable template. ## Create a Blank Scene Create the scene with pixel units for layout and text sizing, add a page, and define the template canvas size. Pairing the design unit with a matching font-size unit keeps `setTextFontSize` values in pixels. ```swift highlight-createTemplateFromScratch-createScene let scene = try engine.scene.create(designUnit: .px, fontSizeUnit: .px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 1000) try engine.block.appendChild(to: scene, child: page) ``` ## Set Page Background Use a color fill for the page background so every generated template starts from the same base appearance. ```swift highlight-createTemplateFromScratch-addBackground let backgroundFill = try engine.block.createFill(.color) try engine.block.setColor( backgroundFill, property: "fill/color/value", color: .rgba(r: 0.98, g: 0.98, b: 0.99, a: 1), ) try engine.block.setFill(page, fill: backgroundFill) ``` The fill stores the color value, and `setFill` assigns that fill to the page. ## Add Text Blocks Text blocks hold the template copy. The sample uses a bundled font, variable tokens for dynamic content, and fixed positions so the layout stays predictable. ```swift highlight-createTemplateFromScratch-addText let brandTypeface = Typeface( name: "Brand Sans", fonts: [ Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ), ], ) let brandRegularFont = brandTypeface.fonts[0] let headline = try engine.block.create(.text) try engine.block.replaceText(headline, text: "{{title}}") try engine.block.setFont(headline, fontFileURL: brandRegularFont.uri, typeface: brandTypeface) try engine.block.setTextFontSize(headline, fontSize: 72) try engine.block.setTextColor(headline, color: .rgba(r: 0.09, g: 0.09, b: 0.09, a: 1)) try engine.block.setPositionX(headline, value: 72) try engine.block.setPositionY(headline, value: 96) try engine.block.setWidth(headline, value: 656) try engine.block.setHeightMode(headline, mode: .auto) try engine.block.appendChild(to: page, child: headline) let subtitle = try engine.block.create(.text) try engine.block.replaceText(subtitle, text: "{{subtitle}}") try engine.block.setFont(subtitle, fontFileURL: brandRegularFont.uri, typeface: brandTypeface) try engine.block.setTextFontSize(subtitle, fontSize: 32) try engine.block.setTextColor(subtitle, color: .rgba(r: 0.32, g: 0.32, b: 0.32, a: 1)) try engine.block.setPositionX(subtitle, value: 72) try engine.block.setPositionY(subtitle, value: 194) try engine.block.setWidth(subtitle, value: 620) try engine.block.setHeightMode(subtitle, mode: .auto) try engine.block.appendChild(to: page, child: subtitle) let cta = try engine.block.create(.text) try engine.block.replaceText(cta, text: "{{cta}}") try engine.block.setFont(cta, fontFileURL: brandRegularFont.uri, typeface: brandTypeface) try engine.block.setTextFontSize(cta, fontSize: 36) try engine.block.setTextColor(cta, color: .rgba(r: 0.09, g: 0.09, b: 0.09, a: 1)) try engine.block.setPositionX(cta, value: 72) try engine.block.setPositionY(cta, value: 858) try engine.block.setWidth(cta, value: 400) try engine.block.setHeightMode(cta, mode: .auto) try engine.block.appendChild(to: page, child: cta) ``` Each text block is appended to the page after its content, typography, position, and sizing are configured. Setting the height mode to `.auto` lets a block grow to fit its resolved text. ## Add Text Variables Variables populate the `{{title}}`, `{{subtitle}}`, and `{{cta}}` tokens in the text blocks. Set defaults while authoring the template so the design has meaningful preview content. ```swift highlight-createTemplateFromScratch-addVariables try engine.variable.set(key: "title", value: "Summer Sale") try engine.variable.set(key: "subtitle", value: "Up to 50% off all items") try engine.variable.set(key: "cta", value: "Learn More") let variableNames = engine.variable.findAll() print("Template variables:", variableNames) ``` When your app applies the template later, call `engine.variable.set(key:value:)` again with the values of your data. `engine.variable.findAll()` returns the keys currently registered in the scene. ## Add Graphic Blocks Graphic blocks are the image containers in the template. Give the image block a name so your app can find it later, then assign a rectangle shape and image fill. ```swift highlight-createTemplateFromScratch-addGraphic let imageBlock = try engine.block.create(.graphic) try engine.block.setName(imageBlock, name: "hero-image") try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(imageBlock, value: 72) try engine.block.setPositionY(imageBlock, value: 300) try engine.block.setWidth(imageBlock, value: 656) try engine.block.setHeight(imageBlock, value: 500) try engine.block.setContentFillMode(imageBlock, mode: .cover) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.appendChild(to: page, child: imageBlock) ``` `setContentFillMode(_:mode:)` set to `.cover` crops the image to fill its bounds. The sample resolves a bundled sample image; in your app, use a stable local or remote URL that can be resolved when the template is loaded. ## Configure Placeholders Placeholder behavior makes the image fill act as swappable content, while placeholder controls let a CE.SDK editor present the block as an interactive drop zone. ```swift highlight-createTemplateFromScratch-configurePlaceholder let placeholderFill = try engine.block.getFill(imageBlock) if try engine.block.supportsPlaceholderBehavior(placeholderFill) { try engine.block.setPlaceholderBehaviorEnabled(placeholderFill, enabled: true) } try engine.block.setPlaceholderEnabled(imageBlock, enabled: true) if try engine.block.supportsPlaceholderControls(imageBlock) { try engine.block.setPlaceholderControlsOverlayEnabled(imageBlock, enabled: true) try engine.block.setPlaceholderControlsButtonEnabled(imageBlock, enabled: true) } ``` Placeholder behavior lives on the fill, so enable it on the fill returned by `getFill(_:)`. Placeholder interaction and controls live on the graphic block itself. Guard the control calls with `supportsPlaceholderControls(_:)` so they only run on blocks that support them. ## Apply Editing Constraints Scopes protect the layout while keeping the image content replaceable. Defer the relevant global scopes to the block level, then disable movement and resizing on the template elements. ```swift highlight-createTemplateFromScratch-applyConstraints try engine.editor.setGlobalScope(key: "layer/move", value: .defer) try engine.editor.setGlobalScope(key: "layer/resize", value: .defer) try engine.editor.setGlobalScope(key: "fill/change", value: .defer) for block in [headline, subtitle, cta, imageBlock] { try engine.block.setScopeEnabled(block, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(block, key: "layer/resize", enabled: false) } try engine.block.setScopeEnabled(imageBlock, key: "fill/change", enabled: true) let imageCanMove = try engine.block.isAllowedByScope(imageBlock, key: "layer/move") let imageFillCanChange = try engine.block.isAllowedByScope(imageBlock, key: "fill/change") print("Image can move:", imageCanMove, "— fill can change:", imageFillCanChange) ``` Deferring a global scope tells the engine to consult each block's own setting for that capability. The image block keeps `fill/change` enabled so users or automation can replace the image without moving the placeholder. `isAllowedByScope(_:key:)` returns the effective permission after resolving both the global and block-level settings. For the full scope and role model, see [Set Editing Constraints](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/set-editing-constraints-c892c0/). ## Save the Template Save the finished scene as a string when its assets stay externally resolvable, or as an archive when you need a portable package with the assets bundled in. ```swift highlight-createTemplateFromScratch-saveTemplate try await engine.block.forceLoadResources([page]) let templateString = try await engine.scene.saveToString() let templateArchive = try await engine.scene.saveToArchive() print("Template string characters:", templateString.count) print("Template archive bytes:", templateArchive.count) ``` `forceLoadResources(_:)` ensures referenced assets are loaded before saving. `saveToString()` is compact and works well for templates stored alongside stable asset URLs. `saveToArchive()` returns a ZIP archive that bundles the assets the engine can access at save time. ## Troubleshooting **Blocks do not appear**: Verify that every page, text block, and graphic block is attached with `appendChild(to:child:)`. **Variables do not resolve**: Check that each text token uses the same key as `engine.variable.set(key:value:)`, including the double curly braces in the text. **The placeholder is not swappable**: Enable placeholder behavior on the fill returned by `getFill(_:)`, not on the graphic block, and enable placeholder interaction on the block. **Constraints are not enforced**: Defer the relevant global scope with `setGlobalScope(key:value:)` set to `.defer` before applying block-level scope settings. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.create(designUnit:fontSizeUnit:)` | Create the empty template scene with pixel measurements and pixel text sizing. | | `engine.block.create(_:)` | Create a page, text, or graphic block. | | `engine.block.setWidth(_:value:)` | Set page or block width. | | `engine.block.setHeight(_:value:)` | Set page or block height. | | `engine.block.setHeightMode(_:mode:)` | Let a text block's height fit its content with `.auto`. | | `engine.block.setPositionX(_:value:)` | Set a block's horizontal position. | | `engine.block.setPositionY(_:value:)` | Set a block's vertical position. | | `engine.block.appendChild(to:child:)` | Add a page or block to the scene hierarchy. | | `engine.block.createFill(_:)` | Create a color or image fill. | | `engine.block.setColor(_:property:color:)` | Set the background fill color. | | `engine.block.setFill(_:fill:)` | Assign a fill to a page or graphic block. | | `engine.block.getFill(_:)` | Read the fill block attached to a graphic block. | | `engine.block.replaceText(_:text:)` | Set text block content. | | `engine.block.setFont(_:fontFileURL:typeface:)` | Apply a typeface to a text block. | | `engine.block.setTextFontSize(_:fontSize:)` | Set text size. | | `engine.block.setTextColor(_:color:)` | Set text color. | | `engine.variable.set(key:value:)` | Set a text variable value. | | `engine.variable.findAll()` | Read the variable keys registered in the scene. | | `engine.block.setName(_:name:)` | Name a block for later lookup. | | `engine.block.createShape(_:)` | Create a rectangular shape for the graphic block. | | `engine.block.setShape(_:shape:)` | Assign the shape to the graphic block. | | `engine.block.setContentFillMode(_:mode:)` | Crop the image to cover its placeholder bounds with `.cover`. | | `engine.block.setURL(_:property:value:)` | Set the image URL on the image fill. | | `engine.block.supportsPlaceholderBehavior(_:)` | Check whether the fill supports placeholder behavior. | | `engine.block.setPlaceholderBehaviorEnabled(_:enabled:)` | Enable placeholder behavior on the fill. | | `engine.block.setPlaceholderEnabled(_:enabled:)` | Enable placeholder interaction on the graphic block. | | `engine.block.supportsPlaceholderControls(_:)` | Check whether the graphic block supports placeholder controls. | | `engine.block.setPlaceholderControlsOverlayEnabled(_:enabled:)` | Show the placeholder overlay. | | `engine.block.setPlaceholderControlsButtonEnabled(_:enabled:)` | Show the placeholder action button. | | `engine.editor.setGlobalScope(key:value:)` | Defer a scope to block-level settings with `.defer`. | | `engine.block.setScopeEnabled(_:key:enabled:)` | Allow or deny a scope on one block. | | `engine.block.isAllowedByScope(_:key:)` | Read the effective permission for a scope on a block. | | `engine.block.forceLoadResources(_:)` | Ensure referenced assets are loaded before saving. | | `engine.scene.saveToString()` | Serialize the template scene as a string. | | `engine.scene.saveToArchive()` | Save the template scene and accessible assets as an archive. | ## Next Steps - [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Configure placeholder behavior and visual controls in depth. - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) - Implement dynamic text personalization with variables. - [Set Editing Constraints](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) - Lock layout properties to protect design integrity. - [Add to Template Library](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-to-template-library-8bfbc7/) - Save and organize templates in an asset source for users to browse and apply. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Import Templates" description: "Load and import design templates into CE.SDK from URLs, archives, and serialized strings." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/import-e50084/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Import Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates/import-e50084/) --- Load design templates into CE.SDK from archive URLs, scene URLs, and serialized strings. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-import-templates) Templates are pre-designed scenes that provide starting points for user projects. CE.SDK supports loading templates from archive URLs with bundled assets, remote scene URLs, or serialized strings stored in databases. ```swift file=@cesdk_swift_examples/engine-guides-import-templates/ImportTemplates.swift reference-only import Foundation import IMGLYEngine @MainActor func importTemplates(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: sceneURL) guard let scene = try engine.scene.get() else { return } let pages = try engine.scene.getPages() print("Template has \(pages.count) page(s)") try await engine.scene.zoom( to: scene, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) // Prepare a serialized scene string for the next section. // In production, sceneString comes from your database or a fetched .scene file. let sceneString = try await engine.scene.saveToString() try await engine.scene.load(from: sceneString) // Prepare a local archive for the next section by saving the current scene. // In production, archiveURL points to your own archive — a remote URL on your // CDN or a local file URL — and load(from:) accepts either. Archives use the // .imgly extension now (.zip remains loadable). let archiveData = try await engine.scene.saveToArchive() let archiveURL = FileManager.default.temporaryDirectory .appendingPathComponent("imported-template-\(UUID().uuidString).imgly") try archiveData.write(to: archiveURL) try await engine.scene.load(from: archiveURL) } ``` This guide covers how to load templates from archives, URLs, and strings, and work with the loaded content. ## Load from Archive Load a template from an archive URL using `load(from:)` — the same call that loads scene files, since the engine detects the file kind automatically. Archives bundle the scene with all its assets, making them portable and self-contained; they use the `.imgly` extension (the `.zip` extension also works). The URL can point to a local file or a remote download — `load(from:)` accepts either. ```swift highlight-importTemplates-loadFromArchive try await engine.scene.load(from: archiveURL) ``` ## Load from URL Load a template from a remote `.scene` file URL using `load(from:)`. The scene file is a JSON-based format that references assets via URLs, so those assets must remain reachable at their original URLs. ```swift highlight-importTemplates-loadFromURL let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: sceneURL) ``` ## Load from String For templates stored in databases or received from APIs, call `load(from:)` with the serialized scene string. This works with content previously produced by `engine.scene.saveToString()`. ```swift highlight-importTemplates-loadFromString try await engine.scene.load(from: sceneString) ``` ## Working with the Loaded Scene After loading a template, retrieve the active scene and inspect or adjust the viewport. ### Verify the Scene Use `engine.scene.get()` to retrieve the current scene block — it returns an optional and is `nil` until a scene has been loaded. Pair it with `engine.scene.getPages()` to list the template's pages; `pages.count` tells you how many it contains. ```swift highlight-importTemplates-getScene guard let scene = try engine.scene.get() else { return } let pages = try engine.scene.getPages() print("Template has \(pages.count) page(s)") ``` ### Zoom to Content Fit the loaded template in the viewport using `engine.scene.zoom(to:)`. The four padding parameters add space in points around the focused block. ```swift highlight-importTemplates-zoomToScene try await engine.scene.zoom( to: scene, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) ``` ## Next Steps - [From Scene File](https://img.ly/docs/cesdk/mac-catalyst/create-templates/import/from-scene-file-52a01e/) — Load and import design templates from scene files in CE.SDK - [Apply Templates](https://img.ly/docs/cesdk/mac-catalyst/use-templates/apply-template-35c73e/) — Apply templates to existing scenes while preserving page dimensions --- ## Related Pages - [Import Templates from Scene Files](https://img.ly/docs/cesdk/mac-catalyst/create-templates/import/from-scene-file-52a01e/) - Load and import design templates from scene files in CE.SDK --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Import Templates from Scene Files" description: "Load and import design templates from scene files in CE.SDK" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/import/from-scene-file-52a01e/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Import Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates/import-e50084/) > [From Scene File](https://img.ly/docs/cesdk/mac-catalyst/create-templates/import/from-scene-file-52a01e/) --- ```swift file=@cesdk_swift_examples/engine-guides-import-from-scene-file/ImportFromSceneFile.swift reference-only import Foundation import IMGLYEngine @MainActor func importFromSceneFile(engine: Engine) async throws { // Base URL the sample templates are resolved against. In your app this is the // location where you host your own `.scene` files. let baseURL = try engine.guidesBaseURL // Prepare a local archive for the next section: load a sample template and // save it as an archive. In production, archiveURL points to your own // archive — a remote URL on your CDN or a local file URL — and load(from:) // accepts either. Archives use the .imgly extension now (.zip remains // loadable). let setupSceneURL = baseURL .appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: setupSceneURL) let archiveData = try await engine.scene.saveToArchive() let archiveURL = FileManager.default.temporaryDirectory .appendingPathComponent("imported-template-\(UUID().uuidString).imgly") try archiveData.write(to: archiveURL) try await engine.scene.load(from: archiveURL) let sceneURL = baseURL .appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: sceneURL) if let loadedPage = try engine.scene.getPages().first { try await engine.captureGuide(loadedPage, label: "after-load-url") } // Create a scene whose page dimensions the template content must adapt to. let designScene = try engine.scene.create() try engine.block.setFloat(designScene, property: "scene/pageDimensions/width", value: 1920) try engine.block.setFloat(designScene, property: "scene/pageDimensions/height", value: 1080) let page = try engine.block.create(.page) try engine.block.appendChild(to: designScene, child: page) let templateURL = baseURL .appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.applyTemplate(from: templateURL) if let appliedPage = try engine.scene.getPages().first { try await engine.captureGuide(appliedPage, label: "hero") } guard let scene = try engine.scene.get() else { return } let pages = try engine.scene.getPages() print("Scene \(scene) contains \(pages.count) page(s)") // Demo: this URL has no scene file behind it. let missingTemplateURL = FileManager.default.temporaryDirectory .appendingPathComponent("missing-template.scene") do { try await engine.scene.load(from: missingTemplateURL) } catch { print("Failed to load template:", error.localizedDescription) } } ``` CE.SDK lets you load complete design templates from scene files to start projects from pre-designed templates, implement template galleries, and build template management systems. ![A business card template applied to a 1920×1080 page, with the template content adjusted to fit the page dimensions.](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-import-from-scene-file) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Scene files are portable design templates that preserve the entire design structure including blocks, assets, styles, and layout. This guide covers loading scenes from archives, loading from URLs, applying templates while preserving dimensions, and understanding scene file formats. ## Scene File Formats CE.SDK supports two scene file formats for importing templates. Both are saved with the `.imgly` extension — `.scene` and `.zip` files also load — and the same `load(from:)` call opens either kind: ### Scene Format Scene files are JSON-based representations of design structures. They reference external assets via URLs, making them lightweight and suitable for database storage. However, the referenced assets must remain accessible at their URLs. **When to use:** - Templates stored in databases - Templates with hosted assets - Lightweight transmission ### Archive Format Archive files are self-contained packages that bundle the scene structure with all referenced assets in a ZIP container. This makes them portable and suitable for offline use. **When to use:** - Template distribution - Offline-capable templates - Complete portability - **Recommended for most use cases** ## Load Scene from Archive The most common way to load templates is from archives. `load(from:)` loads both the scene structure and all embedded assets — the engine detects that the file is an archive automatically — and accepts a local file URL or a remote URL: ```swift highlight-importFromSceneFile-loadFromArchive try await engine.scene.load(from: archiveURL) ``` Here, `archiveURL` is a local file URL the example produces by saving the current scene with `engine.scene.saveToArchive()`. In your app it points to your own archive — a file you bundle or a download from your server. When you load from an archive: - The ZIP file is fetched and extracted - All assets are registered with CE.SDK - The scene structure is loaded - Asset paths are automatically resolved ## Load Scene from URL You can also load scenes directly from `.scene` file URLs using `load(from:)`. This approach requires that all referenced assets remain accessible at their original URLs. In the example, `baseURL` points to the location hosting the sample templates — substitute the URL where you host your own `.scene` files: ```swift highlight-importFromSceneFile-loadFromURL let sceneURL = baseURL .appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: sceneURL) ``` **Important:** With this method, if asset URLs become unavailable, those assets won't load and your template may appear incomplete. ## Apply Template vs Load Scene CE.SDK provides two approaches for working with templates, each serving different purposes: ### Load Scene When you use `load(from:)` with a scene file or an archive, CE.SDK: - Replaces the entire current scene - Adopts the template's page dimensions - Loads all content as-is This is appropriate when starting a new project from a template. ### Apply Template When you use `applyTemplate(from:)`, CE.SDK: - Keeps the design unit and page dimensions of the current scene - Automatically adjusts the template content to fit the new dimensions This is useful when you want to load template content into an existing scene with specific dimensions: ```swift highlight-importFromSceneFile-applyTemplate // Create a scene whose page dimensions the template content must adapt to. let designScene = try engine.scene.create() try engine.block.setFloat(designScene, property: "scene/pageDimensions/width", value: 1920) try engine.block.setFloat(designScene, property: "scene/pageDimensions/height", value: 1080) let page = try engine.block.create(.page) try engine.block.appendChild(to: designScene, child: page) let templateURL = baseURL .appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.applyTemplate(from: templateURL) ``` `applyTemplate(from:)` reads the target dimensions from the scene's `scene/pageDimensions/width` and `scene/pageDimensions/height` properties and resizes the template's pages to match. ## Get Scene Information After loading a template, retrieve the current scene with `engine.scene.get()` — it returns an optional that is `nil` until a scene has been loaded — and list the template's pages with `engine.scene.getPages()`: ```swift highlight-importFromSceneFile-getScene guard let scene = try engine.scene.get() else { return } let pages = try engine.scene.getPages() print("Scene \(scene) contains \(pages.count) page(s)") ``` ## Error Handling `load(from:)` and `applyTemplate(from:)` both throw on failure, so wrap them in `do`/`catch`: ```swift highlight-importFromSceneFile-errorHandling // Demo: this URL has no scene file behind it. let missingTemplateURL = FileManager.default.temporaryDirectory .appendingPathComponent("missing-template.scene") do { try await engine.scene.load(from: missingTemplateURL) } catch { print("Failed to load template:", error.localizedDescription) } ``` ### Network Errors Template URLs might be unreachable. The thrown error describes the failure — show a message to the user and fall back to a default template or an empty scene. ### Invalid Scene Format If the file behind the URL is not a valid scene, the load throws. Validate that uploaded or user-provided files are scene files produced by CE.SDK before offering them as templates. ### Missing Assets For `.scene` files, referenced assets might be unavailable. The scene itself loads, but the affected assets appear missing. Consider using archives to avoid this issue. ## Performance Considerations ### Loading Time Loading time scales with archive size and the number of embedded assets — a small archive loads almost immediately, while a large archive with many bundled assets takes noticeably longer. Actual times depend on the device, storage speed, and whether the assets are already cached, so show a loading indicator for larger templates. ## API Reference | Method | Description | | ----------------------------------- | ---------------------------------------------------------------------------- | | `engine.scene.load(from:)` | Loads a scene or archive from a URL (file kind detected automatically) | | `engine.scene.loadArchive(from:)` | Loads a scene archive from a URL | | `engine.scene.applyTemplate(from:)` | Applies a template while keeping the current design unit and page dimensions | | `engine.scene.saveToArchive()` | Saves the current scene as a self-contained archive | | `engine.scene.get()` | Returns the current scene block, or `nil` if none is loaded | | `engine.scene.getPages()` | Returns all page IDs in the scene | ## Next Steps - [Create From Scratch](https://img.ly/docs/cesdk/mac-catalyst/create-templates/from-scratch-663cda/) — Build reusable design templates programmatically using CE.SDK's APIs - [Apply a Template](https://img.ly/docs/cesdk/mac-catalyst/use-templates/apply-template-35c73e/) — Apply template scenes via API while preserving page dimensions - [Save](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) — Save design progress locally or to a backend service for later editing or publishing --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Lock the Template" description: "Restrict editing access to specific elements or properties in a template to enforce design rules." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Lock the Template](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) --- ```swift file=@cesdk_swift_examples/engine-guides-lock-template/LockTemplate.swift reference-only import Foundation import IMGLYEngine @MainActor func lockTemplate(engine: Engine) throws { let baseURL = try engine.guidesBaseURL let logoImage = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") // Build a brand template with a logo and a headline. New engine instances // start in the default Creator role. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 500) try engine.block.appendChild(to: scene, child: page) let logo = try engine.block.create(.graphic) try engine.block.setShape(logo, shape: engine.block.createShape(.rect)) let logoFill = try engine.block.createFill(.image) try engine.block.setURL(logoFill, property: "fill/image/imageFileURI", value: logoImage) try engine.block.setFill(logo, fill: logoFill) try engine.block.setPositionX(logo, value: 40) try engine.block.setPositionY(logo, value: 40) try engine.block.setWidth(logo, value: 120) try engine.block.setHeight(logo, value: 80) try engine.block.setName(logo, name: "Logo") try engine.block.appendChild(to: page, child: logo) let headline = try engine.block.create(.text) try engine.block.replaceText(headline, text: "Edit this headline") try engine.block.setFloat(headline, property: "text/fontSize", value: 48) try engine.block.setEnum(headline, property: "text/horizontalAlignment", value: "Center") try engine.block.setWidth(headline, value: 720) try engine.block.setHeightMode(headline, mode: .auto) try engine.block.setPositionX(headline, value: 40) try engine.block.setPositionY(headline, value: 200) try engine.block.setName(headline, name: "Headline") try engine.block.appendChild(to: page, child: headline) try engine.editor.setRole("Creator") let activeRole = try engine.editor.getRole() print("Active role:", activeRole) // Creator try engine.block.setScopeEnabled(headline, key: "editor/select", enabled: true) try engine.block.setScopeEnabled(headline, key: "text/edit", enabled: true) let headlineIsSelectable = try engine.block.isScopeEnabled(headline, key: "editor/select") print("Headline editor/select enabled:", headlineIsSelectable) // true try engine.editor.setRole("Adopter") let canEditHeadline = try engine.block.isAllowedByScope(headline, key: "text/edit") let canSelectLogo = try engine.block.isAllowedByScope(logo, key: "editor/select") let canMoveLogo = try engine.block.isAllowedByScope(logo, key: "layer/move") print("Adopter can edit the headline:", canEditHeadline) // true print("Adopter can select the logo:", canSelectLogo) // false print("Adopter can move the logo:", canMoveLogo) // false try engine.editor.setRole("Creator") let creatorCanMoveLogo = try engine.block.isAllowedByScope(logo, key: "layer/move") let headlineStillEditable = try engine.block.isScopeEnabled(headline, key: "text/edit") print("Creator can move the logo:", creatorCanMoveLogo) // true print("Headline text/edit survived the switch:", headlineStillEditable) // true } ``` Set up a two-surface integration where template creators have full editing access while template adopters can only modify designated areas. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-lock-template) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Many integrations need two different editing experiences: one for designers who build templates, and one for end users who customize them. The Creator and Adopter roles make this possible—same CE.SDK, different permissions based on who's using it. For detailed scope configuration patterns, see [Lock Content](https://img.ly/docs/cesdk/mac-catalyst/rules/lock-content-9fa727/). The example builds a brand template with a logo and a headline, marks only the headline as editable, then switches between the Creator and Adopter roles to show how the effective permissions change. ## Understanding the Two-Surface Pattern Template-based workflows typically involve two distinct user groups with different needs: | Surface | Users | Role | What they can do | | --------------- | -------------------- | --------- | --------------------------------------------- | | Creator Surface | Designers, admins | `Creator` | Full editing—build templates, set locks | | Adopter Surface | End users, marketers | `Adopter` | Restricted editing—only modify unlocked areas | This separation protects design intent while enabling customization. Each role installs its own global scope defaults: the Creator role sets every global scope to `.allow`, so block-level locks are never consulted. The Adopter role defers the global scopes to each block's own settings, so the locks configured by creators decide what users can modify. ## Setting Up the Creator Surface The Creator surface is where templates are built. Call `engine.editor.setRole(_:)` with `"Creator"` to give designers unrestricted access. New engine instances already start in the Creator role; the explicit call documents the surface's intent. Read the active role back with `engine.editor.getRole()`. ```swift highlight-lockTemplate-creatorSurface try engine.editor.setRole("Creator") let activeRole = try engine.editor.getRole() print("Active role:", activeRole) // Creator ``` Under the Creator role's defaults, every operation is permitted regardless of block-level scope settings. This is where designers build the template layout, configure which elements should be editable with `engine.block.setScopeEnabled(_:key:enabled:)`, and save the template for distribution. ## Configuring What Users Can Edit The scope system controls what Adopters can modify. While in the Creator role, enable specific scopes on the blocks that should stay editable. Verify a block-level setting with `engine.block.isScopeEnabled(_:key:)`. ```swift highlight-lockTemplate-configureScopes try engine.block.setScopeEnabled(headline, key: "editor/select", enabled: true) try engine.block.setScopeEnabled(headline, key: "text/edit", enabled: true) let headlineIsSelectable = try engine.block.isScopeEnabled(headline, key: "editor/select") print("Headline editor/select enabled:", headlineIsSelectable) // true ``` When Adopters load this template, they can edit the headline text but nothing else. The `editor/select` scope must be enabled for users to interact with a block at all. The logo keeps its defaults—blocks created under the Creator role start with every block-level scope disabled—so it stays locked for Adopters. For comprehensive scope configuration patterns, see [Lock Content](https://img.ly/docs/cesdk/mac-catalyst/rules/lock-content-9fa727/). ## Setting Up the Adopter Surface The Adopter surface is where templates are used. Call `engine.editor.setRole(_:)` with `"Adopter"` to apply the restrictions configured by creators. Check the resulting permissions with `engine.block.isAllowedByScope(_:key:)`, which evaluates the global and block-level settings together. ```swift highlight-lockTemplate-adopterSurface try engine.editor.setRole("Adopter") let canEditHeadline = try engine.block.isAllowedByScope(headline, key: "text/edit") let canSelectLogo = try engine.block.isAllowedByScope(logo, key: "editor/select") let canMoveLogo = try engine.block.isAllowedByScope(logo, key: "layer/move") print("Adopter can edit the headline:", canEditHeadline) // true print("Adopter can select the logo:", canSelectLogo) // false print("Adopter can move the logo:", canMoveLogo) // false ``` The Adopter role sets every global scope to `.defer`—except `editor/add`, which stays `.allow` so users can still add their own content. Blocks an Adopter creates start with all block-level scopes enabled, leaving users in full control of content they add themselves. Scopes describe what users may do: query the effective permission with `isAllowedByScope(_:key:)` and use it to gate the editing controls you expose to users. Engine API calls from your own code aren't blocked by scopes. ## When to Use This Pattern This two-surface approach works well for: - **Brand template systems**: Marketing teams customize approved templates - **Design approval workflows**: Creators build, reviewers can't accidentally modify - **Self-service customization**: End users personalize within guardrails - **White-label products**: Customers can only edit designated areas For simpler use cases where all users have the same permissions, you may not need separate surfaces. For preview or approval surfaces that should allow no editing at all, set the `"Viewer"` role—it sets every global scope to `.deny`. ## Switching Roles at Runtime The same engine instance can switch roles at any time, for example when a designer previews the Adopter experience. Each `setRole(_:)` call re-applies that role's global scope defaults and overwrites any manual `engine.editor.setGlobalScope(key:value:)` customizations. Block-level scopes are untouched, so the locks configured in the Creator role persist across switches. ```swift highlight-lockTemplate-switchRoles try engine.editor.setRole("Creator") let creatorCanMoveLogo = try engine.block.isAllowedByScope(logo, key: "layer/move") let headlineStillEditable = try engine.block.isScopeEnabled(headline, key: "text/edit") print("Creator can move the logo:", creatorCanMoveLogo) // true print("Headline text/edit survived the switch:", headlineStillEditable) // true ``` ## Troubleshooting | Issue | Cause | Solution | | ------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------- | | Adopter can edit everything | The role is still `Creator` | Call `setRole(_:)` with `"Adopter"` on the adopter surface | | Adopter can't edit anything | `editor/select` scope not enabled | Enable `editor/select` on blocks users should interact with | | Manual global scope settings disappear | `setRole(_:)` re-applies the role's global defaults | Re-apply `setGlobalScope(key:value:)` customizations after switching roles | | Changes not persisting | Template not saved after scope changes | Save the template after configuring scopes in the Creator role | ## API Reference | Method | Description | | -------------------------------------------- | ---------------------------------------------------------------------------- | | `engine.editor.setRole(_:)` | Set the editing role: `"Creator"`, `"Adopter"` or `"Viewer"` | | `engine.editor.getRole()` | Get the current editing role | | `engine.block.setScopeEnabled(_:key:enabled:)` | Enable or disable a scope on a block | | `engine.block.isScopeEnabled(_:key:)` | Check if a scope is enabled on a block | | `engine.block.isAllowedByScope(_:key:)` | Check the effective permission after evaluating global and block-level settings | ### Common Scopes | Scope | Description | | --------------------- | ------------------------------------------------------ | | `editor/select` | Allow selecting the block (required for any interaction) | | `fill/change` | Allow changing the block's fill (images, colors) | | `text/edit` | Allow editing text content | | `text/character` | Allow changing text formatting (font, size, color) | | `layer/move` | Allow moving the block | | `layer/resize` | Allow resizing the block | | `layer/rotate` | Allow rotating the block | | `layer/crop` | Allow cropping the block | | `lifecycle/destroy` | Allow deleting the block | ## Next Steps - [Lock Content](https://img.ly/docs/cesdk/mac-catalyst/rules/lock-content-9fa727/) - Configure scope-based permissions to lock design elements - [Set Editing Constraints](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/set-editing-constraints-c892c0/) - Fine-tune what users can modify - [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) - Mark editable image, video, or text areas within a locked template layout --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Overview" description: "Learn how to create, import, and manage reusable templates to streamline design creation in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-templates/overview-4ebe30/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Use Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/create-templates/overview-4ebe30/) --- In CE.SDK, a *template* is a reusable, structured design that defines editable areas and constraints for end users. Templates can be based on static visuals or video compositions and are used to guide content creation, enable mass personalization, and enforce design consistency. Unlike a regular editable design, a template introduces structure through placeholders and constraints, allowing you to define which elements users can change and how. Templates support both static output formats (like PNG, PDF) and videos (like MP4), and can be created or applied using either the CE.SDK UI or API. Templates are a core part of enabling design automation, personalization, and streamlined workflows in any app that includes creative functionality. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## Why Use Templates? Templates are a powerful tool for: - Maintaining **brand consistency** across all user-generated designs. - **Scaling** asset creation for campaigns, catalogs, or print products. - Providing a **guided experience** where users adapt content without starting from scratch. They are ideal for use cases like: - Personalized marketing campaigns - Dynamic social media ads - Product catalogs and e-commerce visuals - Custom print materials and photo books ## Ways to Create Templates You can create templates from scratch or by importing an existing template. **From Scratch:** Start a new project and design a scene with the intent of turning it into a template. You can define variables, placeholders, and constraints directly in the editor. **Import Existing Designs:** If you already have assets created in other tools, you can import them as templates. | Format | Description | | -------- | --------------------------------------------------------------- | | `.idml` | InDesign (via `@imgly/idml-importer`) | | `.psd` | Photoshop (via `@imgly/psd-importer`) | | `.pdf` | PDF (via `@imgly/pdf-importer`) | | `.pptx` | PowerPoint (via `@imgly/pptx-importer`) | | `.imgly` | CE.SDK Native (scene or archive, detected automatically) | | `.scene` | CE.SDK Native (scene extension) | | `.zip` | CE.SDK Native (archive extension) | Non-native design files are converted into editable CE.SDK scenes by dedicated importer packages rather than uploaded as media assets. > **Note:** The `.idml`, `.psd`, `.pdf` and `.pptx` importers ship as separate packages > that you install in addition to CE.SDK. These importer packages run only in > the browser and in Node.js. They are not available on any other platform, > including Android, iOS, Flutter and React Native. > **Note:** Need to import a format not listed here? CE.SDK allows you to create custom > importers for any file type by using our Scene and Block APIs to generate > scenes programmatically. These imported designs can then be adapted into editable, structured templates inside CE.SDK. ## Dynamic Content in Templates Templates support dynamic content to enable data-driven generation of assets. CE.SDK provides several mechanisms: - **Text Variables**: Bind text elements to dynamic values (e.g., user names, product SKUs). - **Image Placeholders**: Reserve space for images to be inserted later. - **Video Placeholders**: Reserve space for videos, enabling dynamic insertion of video clips in a templated layout. This makes it easy to generate hundreds or thousands of personalized variations from a single design. ## Controlling Template Editing Templates in CE.SDK offer powerful tools for controlling the editing experience: - **Editing Constraints**: Lock specific properties such as position, style, or content of elements. - **Locked Templates**: Prevent any edits outside allowed fields to protect design integrity. - **Fully Editable Templates**: Allow unrestricted editing for power users or advanced workflows. - **Form-Based Editing**: Build a custom editing interface for users to fill in variables and placeholders via input forms (a ready-made UI is not currently provided, but can be built using our APIs). These options let you strike a balance between creative freedom and design control. ## Working with Templates Programmatically and Through the UI You can manage templates using both the UI and API: - **UI Integration**: Users can select, apply, and edit templates directly inside the CE.SDK interface. - **Programmatic Access**: Use the SDK’s APIs to create, apply, or modify templates as part of an automated workflow. - **Asset Library Integration**: Templates can appear in the asset library, allowing users to browse and pick templates visually. - The Asset Library's appearance and behavior can be fully customized to fit your app’s needs. ## Managing Templates Templates are saved and reused just like any other CE.SDK asset: - **Save Templates** to a *Template Library*. - **Edit or Remove** existing templates from your asset library. - Templates are saved as Scene (`.scene`) or Archive (`.zip`) files and can be loaded across all platforms supported by CE.SDK (Web, Mobile, Server, Desktop) ## Default and Premium Templates - **Default Templates**: CE.SDK may include a small number of starter templates depending on your configuration. - **Premium Templates**: IMG.LY offers a growing collection of professionally designed templates available for licensing. - Templates can be imported, customized, and used directly within your app. Check your license or speak with our team for details on accessing premium templates. ## Templates as Assets Templates are treated as **assets** in CE.SDK. That means: - They can be included in local or remote asset libraries. - They can be shared, versioned, and indexed using the same systems as images or videos. - You can apply your own metadata, tags, and search capabilities to them. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create Videos" description: "Learn how to create and customize videos in CE.SDK using scenes, assets, and time-based editing." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) --- --- ## Related Pages - [Create Videos Overview](https://img.ly/docs/cesdk/mac-catalyst/create-video/overview-b06512/) - Learn how Swift video projects work in CE.SDK and choose the right guide for UI-based or programmatic video workflows. - [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) - Use the timeline editor to arrange and edit video clips, audio, and animations frame by frame. - [Markers and Frame Rate](https://img.ly/docs/cesdk/mac-catalyst/create-video/markers-and-frame-rate-517cc2/) - Set the frame rate of a scene and name points and spans on its timeline with markers. - [Control Audio and Video](https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/) - Play, pause, seek, and preview audio and video content programmatically in CE.SDK using playback controls and solo mode. - [Trim](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) - Learn how to trim video and audio clips in CE.SDK for Swift by setting trim offsets and trim lengths with the Engine API. - [Force Trim](https://img.ly/docs/cesdk/mac-catalyst/edit-video/force-trim-3c1e8a/) - Enforce minimum and maximum video durations in the editor UI. - [Split Video and Audio](https://img.ly/docs/cesdk/mac-catalyst/edit-video/split-464167/) - Learn how to split video and audio clips at specific time points in CE.SDK for Swift, creating two independent segments from a single clip. - [Join and Arrange Video Clips](https://img.ly/docs/cesdk/mac-catalyst/edit-video/join-and-arrange-3bbc30/) - Combine multiple video clips into sequences and organize them on the timeline using tracks and time offsets in CE.SDK. - [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform-369f28/) - Learn how CE.SDK video transforms use block geometry, crop transforms, groups, animations, and transform permissions. - [Apply Transitions](https://img.ly/docs/cesdk/mac-catalyst/create-video/apply-transitions-146026/) - Blend adjacent video clips with clip-to-clip transitions such as cross-fades, pushes, and wipes using CE.SDK's transitions API. - [Add Captions](https://img.ly/docs/cesdk/mac-catalyst/edit-video/add-captions-f67565/) - Add synchronized captions to video scenes with CE.SDK's caption tracks, caption blocks, subtitle import, styling, and burned-in video export. - [Update Caption Presets](https://img.ly/docs/cesdk/mac-catalyst/create-video/update-caption-presets-e9c385/) - Extend CE.SDK video captions with custom declarative caption style presets using the engine's asset APIs. - [Add Watermark](https://img.ly/docs/cesdk/mac-catalyst/edit-video/add-watermark-762ce6/) - Add text and image watermarks to videos with timeline duration, positioning, opacity, and visibility controls in Swift. - [Annotation](https://img.ly/docs/cesdk/mac-catalyst/edit-video/annotation-e9cbad/) - Add timed text, shapes, and highlights to video scenes with CE.SDK. - [Redact Sensitive Content in Videos](https://img.ly/docs/cesdk/mac-catalyst/edit-video/redaction-cf6d03/) - Redact sensitive video content using blur, pixelization, or solid overlays. Essential for privacy protection when obscuring faces, license plates, or personal information. - [Lock Video Design](https://img.ly/docs/cesdk/mac-catalyst/create-video/lock-design-e92ce4/) - Protect video designs from unwanted modifications using CE.SDK's scope-based permission system. - [Programmatic Creation](https://img.ly/docs/cesdk/mac-catalyst/create-video/programmatic-2b243c/) - Create and export video scenes entirely through code with the CE.SDK Engine on iOS, macOS, and Mac Catalyst. - [Programmatic Editing](https://img.ly/docs/cesdk/mac-catalyst/edit-video/programmatic-8429af/) - Edit video scenes with CE.SDK Engine APIs on Swift. - [Video Editor SDK](https://img.ly/docs/cesdk/mac-catalyst/overview-7d12d5/) - Explore video editing features in CE.SDK including trimming, splitting, captions, and programmatic editing. - [Limitations](https://img.ly/docs/cesdk/mac-catalyst/create-video/limitations-6a740d/) - Understand video resolution, duration, codec, and memory constraints when working with CE.SDK on iOS, Mac Catalyst, and macOS. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Apply Transitions" description: "Blend adjacent video clips with clip-to-clip transitions such as cross-fades, pushes, and wipes using CE.SDK's transitions API." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-video/apply-transitions-146026/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Apply Transitions](https://img.ly/docs/cesdk/mac-catalyst/create-video/apply-transitions-146026/) --- ```swift file=@cesdk_swift_examples/engine-guides-apply-transitions/ApplyTransitions.swift reference-only import Foundation import IMGLYEngine @MainActor func applyTransitions(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 16) let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) let baseURL = try engine.guidesBaseURL let videoURLs = [ "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", "ly.img.video/videos/pexels-tony-schnagl-5528015.mp4", "ly.img.video/videos/pexels-taryn-elliott-8713114.mp4", "ly.img.video/videos/pexels-taryn-elliott-7108801.mp4", ].map { baseURL.appendingPathComponent($0) } var clips = [DesignBlockID]() for (index, videoURL) in videoURLs.enumerated() { let clip = try await makeTransitionClip(engine: engine, videoURL: videoURL, width: 1280, height: 720) try engine.block.appendChild(to: track, child: clip) try engine.block.setDuration(clip, duration: 4) try engine.block.setTimeOffset(clip, offset: Double(index) * 4) clips.append(clip) } try engine.block.fillParent(track) let clipA = clips[0] let clipB = clips[1] let clipC = clips[2] try engine.block.setPlaybackTime(page, time: 3.5) try await engine.captureGuide(page, label: "before-transition") let clipsSupportTransitions = try engine.block.supportsTransition(clipA) && engine.block.supportsTransition(clipB) print("Clips support transitions: \(clipsSupportTransitions)") let crossFade = try engine.block.createTransition(.crossFade) try engine.block.setDuration(crossFade, duration: 1) try engine.block.setTransition(clipA, transition: crossFade) let incomingClipOffset = try engine.block.getTimeOffset(clipB) print("Clip B now starts at \(incomingClipOffset)s (was 4)") let push = try engine.block.createTransition(.push) try engine.block.setDuration(push, duration: 1) try engine.block.setTransition(clipB, transition: push) let pushProperties = try engine.block.findAllProperties(push) print("Push properties: \(pushProperties)") try engine.block.setEnum(push, property: "transition/push/direction", value: "Left") try engine.block.setBool(push, property: "transition/push/morph", value: true) try engine.block.setPlaybackTime(page, time: 6.5) try await engine.captureGuide(page, label: "after-push") let assigned = try engine.block.getTransition(clipA) if engine.block.isValid(assigned) { print("Clip A transitions with: \(try engine.block.getType(assigned))") } let fadeToBlack = try engine.block.createTransition(.fadeToBlack) try engine.block.setDuration(fadeToBlack, duration: 1) try engine.block.setTransition(clipC, transition: fadeToBlack) try engine.block.removeTransition(clipC) print("Detached transition is still valid: \(engine.block.isValid(fadeToBlack))") try engine.block.destroy(fadeToBlack) let colorWipe = try engine.block.createTransition(.colorWipe) try engine.block.setDuration(colorWipe, duration: 1) try engine.block.setTransition(clipC, transition: colorWipe) try engine.block.setEnum(colorWipe, property: "transition/color-wipe/direction", value: "Up") try engine.block.setColor( colorWipe, property: "transition/color-wipe/color", color: .rgba(r: 1, g: 1, b: 1, a: 1), ) // Just before the midpoint of the [9, 10] window: the wipe color covers the frame completely at // the midpoint, so park slightly earlier to catch the band partway across. try engine.block.setPlaybackTime(page, time: 9.45) try await engine.captureGuide(page, label: "after-color-wipe") // Fit the page to the reflowed sequence, then park the playhead inside the first overlap window // so the cross-fade blend is the frame the hero shows. if let lastClip = clips.last { let end = try engine.block.getTimeOffset(lastClip) + engine.block.getDuration(lastClip) try engine.block.setDuration(page, duration: end) } try engine.block.setPlaybackTime(page, time: 3.5) try await engine.captureGuide(page, label: "hero") } @MainActor private func makeTransitionClip( engine: Engine, videoURL: URL, width: Float, height: Float, ) async throws -> DesignBlockID { let clip = try engine.block.create(.graphic) try engine.block.setShape(clip, shape: engine.block.createShape(.rect)) try engine.block.setWidth(clip, value: width) try engine.block.setHeight(clip, value: height) let videoFill = try engine.block.createFill(.video) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(clip, fill: videoFill) try engine.block.setContentFillMode(clip, mode: .cover) try await engine.block.forceLoadAVResource(videoFill) return clip } ``` Blend adjacent video clips into each other with clip-to-clip transitions such as cross-fades, pushes, and wipes using CE.SDK's transitions API. ![Two video clips blending into each other in a cross-fade, halfway through the transition](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-apply-transitions) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. A transition belongs to the **outgoing clip** and blends it into the following clip on the same track. When you assign one, the engine overlaps the two clips for the transition duration: the incoming clip—and every clip after it on that track—moves earlier on the timeline, and audio cross-fades over the same window following the transition's easing curve. Transitions are blocks. You create one with `createTransition(_:)`, attach it to a clip with `setTransition(_:transition:)`, and configure it through the same generic setters you use for other blocks. Once assigned, the transition is owned by its clip: it is destroyed together with the clip. This guide covers how to check transition support, create and assign transitions between clips, understand the timeline overlap they introduce, configure per-type properties, and replace or remove them. ## Creating the Video Timeline Transitions need a video scene with at least two adjacent clips on a track. `supportsTransition(_:)` only reports `true` for clips in a video-mode scene, so create the scene with `createVideo()`. ```swift highlight-applyTransitions-createScene let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 16) let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) ``` Each clip is a graphic block with a video fill. Loading the resource up front with `forceLoadAVResource(_:)` gives the engine the clip's duration and lets it decode frames for playback. ```swift highlight-applyTransitions-clipHelper @MainActor private func makeTransitionClip( engine: Engine, videoURL: URL, width: Float, height: Float, ) async throws -> DesignBlockID { let clip = try engine.block.create(.graphic) try engine.block.setShape(clip, shape: engine.block.createShape(.rect)) try engine.block.setWidth(clip, value: width) try engine.block.setHeight(clip, value: height) let videoFill = try engine.block.createFill(.video) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(clip, fill: videoFill) try engine.block.setContentFillMode(clip, mode: .cover) try await engine.block.forceLoadAVResource(videoFill) return clip } ``` Appending the clips to the track makes them play one after another. Each clip runs for 4 seconds, so they start at 0, 4, 8, and 12 seconds. ```swift highlight-applyTransitions-createClips var clips = [DesignBlockID]() for (index, videoURL) in videoURLs.enumerated() { let clip = try await makeTransitionClip(engine: engine, videoURL: videoURL, width: 1280, height: 720) try engine.block.appendChild(to: track, child: clip) try engine.block.setDuration(clip, duration: 4) try engine.block.setTimeOffset(clip, offset: Double(index) * 4) clips.append(clip) } try engine.block.fillParent(track) let clipA = clips[0] let clipB = clips[1] let clipC = clips[2] ``` ## Checking Transition Support Before assigning a transition, verify that both the outgoing and the incoming clip support one. Any individual clip placed directly in a video track qualifies, no matter whether it shows a video, an image, a shape, a sticker or text. Audio, group, caption, and cutout blocks report `false`, as do blocks outside a video track. ```swift highlight-applyTransitions-checkSupport let clipsSupportTransitions = try engine.block.supportsTransition(clipA) && engine.block.supportsTransition(clipB) print("Clips support transitions: \(clipsSupportTransitions)") ``` `setTransition(_:transition:)` throws when either clip fails this check, so testing upfront lets you disable the option instead of handling an error later. ## Creating and Assigning a Transition Create a cross-fade and give it a duration. The duration defines how long the two clips overlap. ```swift highlight-applyTransitions-createTransition let crossFade = try engine.block.createTransition(.crossFade) try engine.block.setDuration(crossFade, duration: 1) ``` The new block is standalone until you assign it. Assigning it to the first clip blends that clip into the one that follows it on the track. ```swift highlight-applyTransitions-setTransition try engine.block.setTransition(clipA, transition: crossFade) ``` A transition block can only be assigned to one clip. Assigning a different transition to a clip that already has one detaches the previous block without destroying it. ## Understanding the Timeline Overlap Assigning a transition reflows the timeline. The incoming clip moves earlier by the transition duration so the two clips overlap, and all downstream clips on the same track shift with it. The outgoing clip itself stays where it is, and clips on other tracks are untouched. ```swift highlight-applyTransitions-timelineReflow let incomingClipOffset = try engine.block.getTimeOffset(clipB) print("Clip B now starts at \(incomingClipOffset)s (was 4)") ``` With a 1-second cross-fade on the first clip, the second clip's time offset changes from 4 to 3 seconds. The effective overlap is clamped: it never exceeds half of either neighboring clip's duration, so a long transition between short clips plays shorter than requested. Removing the transition restores the original offsets. ## Configuring Transition Properties Each transition type exposes its own properties under `transition/{type}/{property}` keys, which you set through the generic block setters. The key uses the type's short ID, so `TransitionType.push` maps to `transition/push/…`. Use `findAllProperties(_:)` to discover what a specific type offers. ```swift highlight-applyTransitions-configureProperties let push = try engine.block.createTransition(.push) try engine.block.setDuration(push, duration: 1) try engine.block.setTransition(clipB, transition: push) let pushProperties = try engine.block.findAllProperties(push) print("Push properties: \(pushProperties)") try engine.block.setEnum(push, property: "transition/push/direction", value: "Left") ``` Directional types expose a `direction` enum, but they don't all share the same value set: `push`, `slide`, `stack`, `splice`, `wipe`, `gradient-fade`, and `color-wipe` take `Left`, `Right`, `Up`, or `Down`, while `cross-spin` and `chop` take `Clockwise` or `CounterClockwise`, `diagonal-splice` takes `RaisedRamp` or `LoweredRamp`, and `two-stripes` takes `Horizontal` or `Vertical`. Call `getEnumValues(ofProperty:)` to read the accepted values for a key. Other types expose numeric controls such as `transition/cross-spin/intensity` or `transition/cross-warp/zoom`, and `color-wipe` exposes a `transition/color-wipe/color` color property. ## Morphing Between Clips With morph enabled, the engine interpolates position, rotation, scale, and shape between the outgoing and incoming clip during the transition. Most types morph by default; `push` is the exception and starts with morph off, and `clock-wipe` and `cross-spin` don't expose the flag at all. ```swift highlight-applyTransitions-morph try engine.block.setBool(push, property: "transition/push/morph", value: true) ``` Morphing is most visible when the two clips differ in framing—for example when one clip is scaled or rotated relative to the other. It only works between two graphic clips. With a text clip on either side, the transition plays without morphing, whatever the flag says. On iOS, the editor offers no Morph control in that case. ## Reading Back a Transition `getTransition(_:)` returns the transition assigned to a clip, or an invalid block when the clip has none. Check the result with `isValid(_:)` before using it. ```swift highlight-applyTransitions-getTransition let assigned = try engine.block.getTransition(clipA) if engine.block.isValid(assigned) { print("Clip A transitions with: \(try engine.block.getType(assigned))") } ``` ## Replacing or Removing a Transition `removeTransition(_:)` detaches a clip's transition and restores the original clip timing, but does not destroy the detached block. Destroy it yourself when you no longer need it, then assign a replacement. ```swift highlight-applyTransitions-removeTransition let fadeToBlack = try engine.block.createTransition(.fadeToBlack) try engine.block.setDuration(fadeToBlack, duration: 1) try engine.block.setTransition(clipC, transition: fadeToBlack) try engine.block.removeTransition(clipC) print("Detached transition is still valid: \(engine.block.isValid(fadeToBlack))") try engine.block.destroy(fadeToBlack) let colorWipe = try engine.block.createTransition(.colorWipe) try engine.block.setDuration(colorWipe, duration: 1) try engine.block.setTransition(clipC, transition: colorWipe) try engine.block.setEnum(colorWipe, property: "transition/color-wipe/direction", value: "Up") try engine.block.setColor( colorWipe, property: "transition/color-wipe/color", color: .rgba(r: 1, g: 1, b: 1, a: 1), ) ``` Removing from a clip without an assigned transition is a no-op. ## Transition Types `createTransition(_:)` takes a `TransitionType`. Its `rawValue` is the longhand ID—`//ly.img.ubq/transition/cross-fade` for `.crossFade`—and the short form of that ID is what the property keys are built from. `TransitionType` conforms to `CaseIterable`, so you can enumerate every type to build a picker. - **Blends**: `.crossFade`, `.crossBlur`, `.crossSpin`, `.crossZoom`, `.crossWarp` - **Movement**: `.push`, `.slide`, `.stack`, `.splice`, `.diagonalSplice` - **Fades**: `.fade`, `.fadeToBlack`, `.fadeToWhite`, `.gradientFade` - **Wipes**: `.wipe`, `.lineWipe`, `.clockWipe`, `.colorWipe` - **Patterns**: `.chop`, `.twoStripes` ## Troubleshooting ### Transition Has No Visible Effect Check that both clips sit next to each other on the same track and that the playhead is inside the overlap window. The overlap starts at the incoming clip's time offset and ends at the outgoing clip's end. ### setTransition Throws Verify `supportsTransition(_:)` returns `true` for both the outgoing and the incoming clip, and that the transition block isn't already assigned to another clip. The outgoing clip also needs a clip following it on the same track, and the call requires the `appearance/animation` scope. ### Transition Disappeared The engine destroys an assigned transition when the owning clip is destroyed, or when the two clips stop being timeline-adjacent. Adjacency only breaks on a track you manage yourself: a track keeps its children packed while `track/automaticallyManageBlockOffsets` is `true`, which is the default. ### Transition Plays Shorter Than Requested The effective duration is clamped to half of either neighboring clip's duration. Shorten the transition or lengthen the clips. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.createTransition(_:)` | Create a standalone transition block of a `TransitionType` | | `engine.block.supportsTransition(_:)` | Check whether a clip can own an outgoing transition | | `engine.block.setTransition(_:transition:)` | Assign the outgoing transition of a clip | | `engine.block.getTransition(_:)` | Get the assigned transition, or an invalid block when unset | | `engine.block.removeTransition(_:)` | Detach the outgoing transition of a clip | | `engine.block.setDuration(_:duration:)` | Set clip or transition duration in seconds | | `engine.block.getTimeOffset(_:)` | Get a clip's timeline start | | `engine.block.findAllProperties(_:)` | List the properties a transition exposes | | `engine.block.getEnumValues(ofProperty:)` | List the accepted values of an enum property | | `engine.block.isValid(_:)` | Check a `getTransition(_:)` result | | `engine.block.destroy(_:)` | Destroy a detached transition block | ### Properties | Property | Type | Description | | --- | --- | --- | | `transition/{type}/direction` | Enum | Direction of a directional transition | | `transition/{type}/morph` | Bool | Interpolate position, rotation, scale, and shape between the clips | | `transition/color-wipe/color` | Color | Color of the wipe | | `transition/cross-spin/intensity` | Float | Strength of the spin | | `transition/cross-warp/zoom` | Float | Zoom applied while warping | ## Next Steps - [Join and Arrange Video Clips](https://img.ly/docs/cesdk/mac-catalyst/edit-video/join-and-arrange-3bbc30/) - Build the clip sequence transitions operate on - [Trim Video Clips](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) - Control which portion of media plays back - [Create Animations](https://img.ly/docs/cesdk/mac-catalyst/animation/create-15cf50/) - Entrance and exit effects for individual blocks --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Control Audio and Video" description: "Play, pause, seek, and preview audio and video content programmatically in CE.SDK using playback controls and solo mode." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Control Audio and Video](https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/) --- Play, pause, seek, and preview audio and video content programmatically using CE.SDK's playback control APIs. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-video-control) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. CE.SDK provides playback control for audio and video through the Block API. Playback state, seeking, and solo preview are controlled programmatically. Resources must be loaded before accessing metadata like duration and dimensions. ```swift file=@cesdk_swift_examples/engine-guides-create-video-control/ControlAVPlayback.swift reference-only import Foundation import IMGLYEngine @MainActor func controlAVPlayback(engine: Engine) async throws { // Demo scaffolding: a video scene with a single video block on a track. let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) let baseURL = try engine.guidesBaseURL let videoBlock = try engine.block.create(.graphic) try engine.block.setShape(videoBlock, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(videoBlock, fill: videoFill) let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) try engine.block.appendChild(to: track, child: videoBlock) try engine.block.fillParent(track) try engine.block.setDuration(videoBlock, duration: 10) try await engine.block.forceLoadAVResource(videoFill) let videoWidth = try engine.block.getVideoWidth(videoFill) let videoHeight = try engine.block.getVideoHeight(videoFill) let totalDuration = try engine.block.getAVResourceTotalDuration(videoFill) _ = (videoWidth, videoHeight, totalDuration) if try engine.block.supportsPlaybackControl(page) { let isPlaying = try engine.block.isPlaying(page) try engine.block.setPlaying(page, enabled: !isPlaying) } var currentTime: Double = 0 if try engine.block.supportsPlaybackTime(page) { try engine.block.setPlaybackTime(page, time: 3.0) currentTime = try engine.block.getPlaybackTime(page) } _ = currentTime let isVisible = try engine.block.isVisibleAtCurrentPlaybackTime(videoBlock) _ = isVisible try engine.block.setSoloPlaybackEnabled(videoFill, enabled: true) let soloEnabled = try engine.block.isSoloPlaybackEnabled(videoFill) try engine.block.setSoloPlaybackEnabled(videoFill, enabled: false) _ = soloEnabled } ``` This guide covers how to play and pause media, seek to specific positions, preview individual blocks with solo mode, check visibility at playback time, and access video resource metadata. ## Force Loading Resources Media resource metadata is unavailable until the resource is loaded. Call `forceLoadAVResource(_:)` on the video fill to ensure dimensions and duration are accessible. The method is `async throws` — the call suspends until the resource finishes loading. ```swift highlight-controlAV-forceLoad try await engine.block.forceLoadAVResource(videoFill) ``` Without loading the resource first, accessing properties like duration or dimensions throws an error. ## Getting Video Metadata Once the resource is loaded, query the video dimensions and total duration. ```swift highlight-controlAV-metadata let videoWidth = try engine.block.getVideoWidth(videoFill) let videoHeight = try engine.block.getVideoHeight(videoFill) let totalDuration = try engine.block.getAVResourceTotalDuration(videoFill) ``` `getVideoWidth(_:)` and `getVideoHeight(_:)` return the original video dimensions in pixels as `Int`. `getAVResourceTotalDuration(_:)` returns the full duration of the source media in seconds as a `Double`. ## Playing and Pausing Check if the block supports playback control with `supportsPlaybackControl(_:)`, then start or stop playback with `setPlaying(_:enabled:)`. `isPlaying(_:)` returns the current playback state. ```swift highlight-controlAV-playbackControl if try engine.block.supportsPlaybackControl(page) { let isPlaying = try engine.block.isPlaying(page) try engine.block.setPlaying(page, enabled: !isPlaying) } ``` Pass the page (or scene) to control timeline playback. Audio blocks and video fills also accept these calls when you want to drive a single clip independently. ## Seeking Jump to a specific playback position with `setPlaybackTime(_:time:)`. Check `supportsPlaybackTime(_:)` first to confirm the block is on the timeline. `getPlaybackTime(_:)` returns the current position. ```swift highlight-controlAV-seeking var currentTime: Double = 0 if try engine.block.supportsPlaybackTime(page) { try engine.block.setPlaybackTime(page, time: 3.0) currentTime = try engine.block.getPlaybackTime(page) } ``` Playback time is measured in seconds from the start of the timeline. ## Visibility at Current Time Use `isVisibleAtCurrentPlaybackTime(_:)` to check whether a block is visible on the canvas at the current playback position. This is useful when blocks have different time offsets or durations. ```swift highlight-controlAV-visibility let isVisible = try engine.block.isVisibleAtCurrentPlaybackTime(videoBlock) ``` ## Solo Playback Solo playback previews an individual block while the rest of the scene stays frozen. Enable it on a video fill or audio block with `setSoloPlaybackEnabled(_:enabled:)`. ```swift highlight-controlAV-solo try engine.block.setSoloPlaybackEnabled(videoFill, enabled: true) let soloEnabled = try engine.block.isSoloPlaybackEnabled(videoFill) try engine.block.setSoloPlaybackEnabled(videoFill, enabled: false) ``` Setting solo to `true` on one block automatically sets it to `false` on every other block. Query the current state with `isSoloPlaybackEnabled(_:)`. ## Troubleshooting ### Properties Unavailable Before Resource Load **Symptom**: Accessing duration, dimensions, or trim values throws an error. **Cause**: Media resource not yet loaded. **Solution**: Always `await engine.block.forceLoadAVResource(_:)` before accessing these properties. ### Block Not Playing **Symptom**: Calling `setPlaying(_:enabled:)` has no effect. **Cause**: Block doesn't support playback control or scene not in playback mode. **Solution**: Check `supportsPlaybackControl(_:)` returns `true`; ensure scene playback is active. ### Solo Playback Not Working **Symptom**: Enabling solo doesn't isolate the block. **Cause**: Solo applied to wrong block type or block not visible. **Solution**: Apply solo to video fills or audio blocks; ensure the block is at the current playback time. ## API Reference | Method | Purpose | | --- | --- | | `setPlaying(_:enabled:)` | Enable or disable block playback | | `isPlaying(_:)` | Check whether the block is playing | | `setSoloPlaybackEnabled(_:enabled:)` | Enable or disable solo playback mode | | `isSoloPlaybackEnabled(_:)` | Check whether solo playback is enabled | | `supportsPlaybackTime(_:)` | Check whether the block has a playback time property | | `setPlaybackTime(_:time:)` | Set the current playback position in seconds | | `getPlaybackTime(_:)` | Get the current playback position in seconds | | `isVisibleAtCurrentPlaybackTime(_:)` | Check whether the block is visible at the current playback time | | `supportsPlaybackControl(_:)` | Check whether the block supports playback control | | `forceLoadAVResource(_:)` | Force load media resource metadata (async) | | `getAVResourceTotalDuration(_:)` | Get the source media duration in seconds | | `getVideoWidth(_:)` | Get the video width in pixels | | `getVideoHeight(_:)` | Get the video height in pixels | ## Next Steps - [Trim Video and Audio](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) — Control which portion of source media plays - [Loop Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/loop-937be7/) — Enable repeating playback for audio blocks - [Adjust Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/) — Control audio volume and muting - [Adjust Speed](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-speed-908d57/) — Change playback speed for audio - [Video Timeline Overview](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) — Timeline editing system --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Limitations" description: "Understand video resolution, duration, codec, and memory constraints when working with CE.SDK on iOS, Mac Catalyst, and macOS." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-video/limitations-6a740d/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Limitations](https://img.ly/docs/cesdk/mac-catalyst/create-video/limitations-6a740d/) --- CE.SDK processes video on the device, providing privacy and responsiveness while operating within hardware and memory constraints. This reference covers resolution limits, codec support, and platform considerations so you can plan video workflows that run reliably on Apple devices. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-limitations) Client-side video processing keeps user content on the device and avoids round trips to a server, but it depends on the device's CPU, GPU, and memory. Understanding these constraints helps you build apps that perform reliably across iPhone, iPad, and Mac configurations. ```swift file=@cesdk_swift_examples/engine-guides-limitations/Limitations.swift reference-only import Foundation import IMGLYEngine @MainActor func limitations(engine: Engine) async throws { let maxExportSize = try engine.editor.getMaxExportSize() let usedMemory = try engine.editor.getUsedMemory() let usedMemoryMB = Double(usedMemory) / (1024 * 1024) let availableMemory = try? engine.editor.getAvailableMemory() let availableMemoryMB = availableMemory.map { Double($0) / (1024 * 1024) } let memoryUtilization: Double? = availableMemory.map { available in let total = Double(usedMemory + available) return total > 0 ? (Double(usedMemory) / total) * 100 : 0 } let desiredWidth = 3840 let desiredHeight = 2160 let limitKnown = maxExportSize < Int(Int32.max) let canExport4K = limitKnown && desiredWidth <= maxExportSize && desiredHeight <= maxExportSize _ = usedMemoryMB _ = availableMemoryMB _ = memoryUtilization _ = limitKnown _ = canExport4K } ``` This guide covers resolution and duration limits, codec support, hardware considerations, and the engine APIs you can call at runtime to query the current device's capabilities. ## Resolution Limits Video resolution depends on the device's hardware and available memory. CE.SDK supports up to 4K UHD for playback and export on capable devices. Import resolution has no fixed cap, but very large videos consume proportional amounts of memory while decoding and editing. Playback and export at 4K require enough GPU bandwidth and texture memory to keep up with the source. Query the maximum export size before initiating an export to avoid failures: ```swift highlight-limitations-queryMaxExportSize let maxExportSize = try engine.editor.getMaxExportSize() ``` `getMaxExportSize()` returns a single pixel limit that applies to both width and height. When the engine cannot read a cap from the active render target it returns `Int32.max` as a sentinel meaning the limit is unknown — not unlimited. Treat this case conservatively: the actual GPU and memory ceilings still apply, and exports may fail even when both dimensions are below `Int32.max`. Gate the feasibility check on the sentinel before comparing dimensions: ```swift highlight-limitations-checkExportFeasibility let desiredWidth = 3840 let desiredHeight = 2160 let limitKnown = maxExportSize < Int(Int32.max) let canExport4K = limitKnown && desiredWidth <= maxExportSize && desiredHeight <= maxExportSize ``` ## Duration Limits Video duration affects editing responsiveness and export time. CE.SDK optimizes for short-form content while supporting longer videos with performance trade-offs. Stories and reels up to 2 minutes are fully supported with smooth editing performance. Videos up to 10 minutes work well on modern hardware, with export times typically around one minute for this length. Longer videos are technically possible but may impact editing responsiveness on less capable devices. For long-form content, consider these approaches: - Split longer videos into shorter segments for editing - Use lower resolution previews during editing, then export at full quality - Test on target devices to establish acceptable duration limits for your use case ## Frame Rate Support Frame rate affects both playback smoothness and export performance. Hardware acceleration significantly impacts high frame rate capabilities. 30 FPS at 1080p is broadly supported and provides smooth playback on most devices. 60 FPS and high-resolution combinations benefit from hardware acceleration through VideoToolbox on Apple platforms. Variable frame rate sources may have timing precision limitations — for best results, consider transcoding variable frame rate content to constant frame rate before importing. ## Supported Codecs CE.SDK supports widely-adopted video and audio codecs through the system's media frameworks. ### Video Codecs - **H.264 / AVC** in `.mp4` containers — universal support - **H.265 / HEVC** in `.mp4` containers — supported natively on Apple platforms via VideoToolbox ### Audio Codecs - **MP3** in `.mp3` files or within `.mp4` containers - **AAC** in `.m4a`, `.mp4`, or `.mov` containers Codec decoding and encoding run on the system's media frameworks, so support is consistent across iPhone, iPad, and Mac as long as the underlying OS version provides the codec. ## Hardware Requirements Device capabilities directly affect video processing performance. CE.SDK scales with available hardware resources. ### Recommended Hardware | Device class | Minimum | | --------------------------- | ------------------------------------------------------------------------ | | iPhone | iPhone 8 or newer | | iPad | iPad (6th gen) or newer | | Mac / Mac Catalyst | Mac released in 2018 or later with at least 4 GB of memory | ### GPU Considerations Hardware acceleration improves both decoding and encoding performance, and high-resolution or high-frame-rate exports benefit most from GPU support. Apple Silicon devices provide unified memory between the CPU and GPU, which simplifies high-resolution workflows. The maximum export dimension reported by `getMaxExportSize()` depends on the GPU's maximum texture size and varies between device generations. ## Memory Constraints Client-side video processing operates within the app's available memory. The engine exposes two query APIs you can use to observe consumption as a coarse signal for telemetry, heuristics, and debugging. Query current memory usage to understand how much has been consumed: ```swift highlight-limitations-queryMemoryUsage let usedMemory = try engine.editor.getUsedMemory() let usedMemoryMB = Double(usedMemory) / (1024 * 1024) ``` Check how much memory remains available for additional resources: ```swift highlight-limitations-queryAvailableMemory let availableMemory = try? engine.editor.getAvailableMemory() let availableMemoryMB = availableMemory.map { Double($0) / (1024 * 1024) } ``` `getAvailableMemory()` is unavailable on the iOS Simulator — the underlying memory API behaves differently in the simulated environment. The example wraps the call with `try?` so it returns `nil` on the Simulator and a byte count on real iOS devices, Mac Catalyst, and macOS. `try?` also swallows any other thrown error, so a `nil` result is not by itself proof that the host is the Simulator — use `do/catch` if you need to log the underlying error message. On a real device, calculate the utilization percentage from the used and available values: ```swift highlight-limitations-calculateMemoryPercentage let memoryUtilization: Double? = availableMemory.map { available in let total = Double(usedMemory + available) return total > 0 ? (Double(usedMemory) / total) * 100 : 0 } ``` Both calls are process- and OS-level: `getUsedMemory()` reports the full process footprint (everything the app has allocated, not just the engine), and `getAvailableMemory()` reports memory still available to the process — on iOS devices this is the per-process OS budget, while on Mac and Mac Catalyst it is device-wide free RAM read via Mach host statistics and fluctuates with other apps' usage. The percentage therefore means different things across Apple targets: on iOS devices it tracks how close the app is to its own memory ceiling and rises with app pressure; on Mac and Mac Catalyst it reflects how much physical RAM is unused system-wide, so a low value mostly signals that other apps are busy rather than that this app is under pressure. Treat the ratio as a coarse process-level signal, not an engine-internal one. Multiple video tracks, effects, and large source assets all increase memory usage proportionally. The engine flags `getUsedMemory()` and `getAvailableMemory()` as testing-and-debugging signals whose results may be unreliable — use them for telemetry and coarse heuristics, not as a hard pre-load gate. Plan capacity based on target-device profiling instead. ## Export Size Limitations Export dimensions are bounded by GPU texture size limits. Always query `getMaxExportSize()` before initiating exports to ensure the requested dimensions are supported. The maximum export size varies by device GPU capabilities. Common limits include: - **4096 pixels**: older iPhones and iPads - **8192 pixels**: most modern iPhones, iPads, and Intel Macs - **16384 pixels**: high-end Mac configurations with discrete or Apple Silicon GPUs Consider target playback requirements when planning export dimensions. Mobile playback and most streaming platforms rarely benefit from resolutions above 1080p or 4K, so exporting at extreme resolutions may not provide practical value. ## Troubleshooting Common issues developers encounter related to video limitations: | Issue | Cause | Solution | | ------------------------------------ | ---------------------------------------------------- | ----------------------------------------------------------------- | | Slow playback at high resolution | Hardware cannot keep up with decoding | Reduce preview resolution or use proxy editing | | Export fails with large video | Memory limits exceeded | Reduce resolution or split the video into shorter segments | | Export size rejected | Exceeds device GPU texture limits | Query `getMaxExportSize()` and reduce target dimensions | | `getAvailableMemory()` always throws | Running on the iOS Simulator | Test on a real device, or treat the `try?` `nil` path as the no-value branch | | High memory usage during editing | Multiple video tracks or high-resolution assets | Lower `maxImageSize`, downscale source assets, or remove tracks | | Slow export on older devices | Older GPU with limited hardware acceleration | Export at 1080p instead of 4K, or shorten the video | ## API Reference | Method | Description | | ------------------------------------ | -------------------------------------------------------- | | `engine.editor.getMaxExportSize()` | Returns the maximum export dimension in pixels for the current device | | `engine.editor.getUsedMemory()` | Returns the process's physical memory footprint in bytes on Apple platforms | | `engine.editor.getAvailableMemory()` | Returns memory available to the process in bytes — per-process OS budget on iOS device, device-wide free RAM on Mac and Mac Catalyst; throws on the iOS Simulator | ## Next Steps Explore related guides to build complete video workflows: - [Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/) — Configure `maxImageSize` and handle export failures with retries - [Video Overview](https://img.ly/docs/cesdk/mac-catalyst/create-video/overview-b06512/) — Fundamentals of editing video with CE.SDK - [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/file-format-support-3c4b2a/) — Detailed compatibility matrix for images, videos, and audio - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Fundamentals of exporting from CE.SDK --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Lock Video Design" description: "Protect video designs from unwanted modifications using CE.SDK's scope-based permission system." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-video/lock-design-e92ce4/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Lock Design](https://img.ly/docs/cesdk/mac-catalyst/create-video/lock-design-e92ce4/) --- ```swift file=@cesdk_swift_examples/engine-guides-lock-video-design/LockVideoDesign.swift reference-only import Foundation import IMGLYEngine @MainActor func lockVideoDesign(engine: Engine) async throws { // Build a small video scene: one page with a track that holds one video // clip, plus an editable title overlay and a locked watermark overlay. // The blocks below are reference context for the scope calls in the // highlighted sections. let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 12) let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) let baseURL = try engine.guidesBaseURL let videoClip = try engine.block.create(.graphic) try engine.block.setShape(videoClip, shape: engine.block.createShape(.rect)) try engine.block.setDuration(videoClip, duration: 12) let videoFill = try engine.block.createFill(.video) try engine.block.setURL( videoFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent("ly.img.video/videos/pexels-kampus-production-8154913.mp4"), ) try engine.block.setFill(videoClip, fill: videoFill) try engine.block.appendChild(to: track, child: videoClip) try engine.block.fillParent(track) let titleOverlay = try engine.block.create(.text) try engine.block.appendChild(to: page, child: titleOverlay) try engine.block.setWidthMode(titleOverlay, mode: .auto) try engine.block.setHeightMode(titleOverlay, mode: .auto) try engine.block.setPositionX(titleOverlay, value: 80) try engine.block.setPositionY(titleOverlay, value: 80) try engine.block.setDuration(titleOverlay, duration: 12) try engine.block.replaceText(titleOverlay, text: "Editable title") let watermarkOverlay = try engine.block.create(.text) try engine.block.appendChild(to: page, child: watermarkOverlay) try engine.block.setWidthMode(watermarkOverlay, mode: .auto) try engine.block.setHeightMode(watermarkOverlay, mode: .auto) try engine.block.setPositionX(watermarkOverlay, value: 980) try engine.block.setPositionY(watermarkOverlay, value: 640) try engine.block.setDuration(watermarkOverlay, duration: 12) try engine.block.replaceText(watermarkOverlay, text: "LOCKED") let scopes = engine.editor.findAllScopes() for scope in scopes { try engine.editor.setGlobalScope(key: scope, value: .deny) } try engine.editor.setGlobalScope(key: "editor/select", value: .defer) try engine.block.setScopeEnabled(videoClip, key: "editor/select", enabled: true) try engine.block.setScopeEnabled(titleOverlay, key: "editor/select", enabled: true) try engine.block.setScopeEnabled(watermarkOverlay, key: "editor/select", enabled: false) try engine.editor.setGlobalScope(key: "text/edit", value: .defer) try engine.editor.setGlobalScope(key: "text/character", value: .defer) try engine.block.setScopeEnabled(titleOverlay, key: "text/edit", enabled: true) try engine.block.setScopeEnabled(titleOverlay, key: "text/character", enabled: true) try engine.editor.setGlobalScope(key: "fill/change", value: .defer) try engine.block.setScopeEnabled(videoClip, key: "fill/change", enabled: true) try engine.editor.setGlobalScope(key: "layer/move", value: .defer) try engine.editor.setGlobalScope(key: "layer/resize", value: .defer) try engine.editor.setGlobalScope(key: "layer/rotate", value: .defer) try engine.block.setScopeEnabled(titleOverlay, key: "layer/move", enabled: true) try engine.block.setScopeEnabled(titleOverlay, key: "layer/resize", enabled: true) try engine.block.setScopeEnabled(titleOverlay, key: "layer/rotate", enabled: true) let lockedOverlayScopes = [ "text/edit", "text/character", "fill/change", "layer/move", "layer/resize", "layer/rotate", ] for scope in lockedOverlayScopes { try engine.block.setScopeEnabled(watermarkOverlay, key: scope, enabled: false) } let canSelectVideoClip = try engine.block.isAllowedByScope(videoClip, key: "editor/select") let canReplaceVideoClip = try engine.block.isAllowedByScope(videoClip, key: "fill/change") let canMoveVideoClip = try engine.block.isAllowedByScope(videoClip, key: "layer/move") let canEditTitle = try engine.block.isAllowedByScope(titleOverlay, key: "text/edit") let canMoveTitle = try engine.block.isAllowedByScope(titleOverlay, key: "layer/move") let canSelectWatermark = try engine.block.isAllowedByScope(watermarkOverlay, key: "editor/select") let titleTextEditEnabled = try engine.block.isScopeEnabled(titleOverlay, key: "text/edit") let textEditGlobalScope = try engine.editor.getGlobalScope(key: "text/edit") print("Permission status:") print("- Can select video clip:", canSelectVideoClip) // true print("- Can replace video clip fill:", canReplaceVideoClip) // true print("- Can move video clip:", canMoveVideoClip) // false print("- Can edit title:", canEditTitle) // true print("- Can move title:", canMoveTitle) // true print("- Can select watermark:", canSelectWatermark) // false print("- Title text/edit block scope enabled:", titleTextEditEnabled) // true print("- text/edit global is .defer:", textEditGlobalScope == .defer) // true let availableScopes = engine.editor.findAllScopes() print("Available scopes:", availableScopes) for scope in availableScopes { let globalSetting = try engine.editor.getGlobalScope(key: scope) print("- \(scope) is .defer:", globalSetting == .defer) } } ``` Protect video clips, overlays, and placeholders from unwanted edits using CE.SDK's scope-based permission system. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-lock-video-design) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. CE.SDK uses global scopes and block-level scopes to decide which editing operations are allowed. For video designs, the same model can lock the whole scene, keep watermarks protected, and make only selected clips or overlays editable. The backing sample creates a small headless video scene with one clip, one editable title overlay, and one locked watermark. Those blocks provide context for the scope calls below. ## Understanding Scope Permissions Scopes control what operations users can perform on video clips, text overlays, watermarks, and other design blocks. CE.SDK combines global scope settings with block-level settings to determine the effective permission. | Global Scope | Block Scope | Result | | ------------ | ----------- | --------- | | `.allow` | any | Permitted | | `.deny` | any | Blocked | | `.defer` | enabled | Permitted | | `.defer` | disabled | Blocked | Global scopes have three possible values: - **`.allow`**: The operation is always permitted, regardless of block-level settings - **`.deny`**: The operation is always blocked, regardless of block-level settings - **`.defer`**: The permission depends on the block-level scope setting Block-level scopes are binary. They only take effect when the matching global scope is set to `.defer`. ## Lock the Entire Video Design To lock all editing operations, discover the current scope keys with `engine.editor.findAllScopes()` and set each global scope to `.deny`. ```swift highlight-lockVideoDesign-lockEntireDesign let scopes = engine.editor.findAllScopes() for scope in scopes { try engine.editor.setGlobalScope(key: scope, value: .deny) } ``` When all scopes are denied, users cannot select, move, edit text, replace fills, or delete blocks. This also prevents changes to video clips and overlays until you defer the global scope and enable the matching block-level scope on the blocks you want editable. ## Enable Selection for Editable Video Blocks Before users can interact with any block, enable `editor/select`. Setting the global scope to `.defer` delegates the decision to each block, so only selected clips or overlays become interactive. ```swift highlight-lockVideoDesign-enableSelection try engine.editor.setGlobalScope(key: "editor/select", value: .defer) try engine.block.setScopeEnabled(videoClip, key: "editor/select", enabled: true) try engine.block.setScopeEnabled(titleOverlay, key: "editor/select", enabled: true) try engine.block.setScopeEnabled(watermarkOverlay, key: "editor/select", enabled: false) ``` ## Selective Video Locking Patterns Lock everything first, then selectively enable the capabilities that each video block needs. This keeps the default state restrictive while allowing controlled editing. ### Text Overlay Editing Enable `text/edit` for text changes and `text/character` when users should also adjust text styling. The sample applies both scopes only to the title overlay. ```swift highlight-lockVideoDesign-textOverlayEditing try engine.editor.setGlobalScope(key: "text/edit", value: .defer) try engine.editor.setGlobalScope(key: "text/character", value: .defer) try engine.block.setScopeEnabled(titleOverlay, key: "text/edit", enabled: true) try engine.block.setScopeEnabled(titleOverlay, key: "text/character", enabled: true) ``` The title text can now be edited while unrelated layout, fill, and lifecycle operations stay locked unless another section enables them. ### Video Clip Replacement Enable `fill/change` on a video placeholder when users may replace the media source but should not change layout. The sample keeps movement, resize, and rotation denied on the video clip. ```swift highlight-lockVideoDesign-videoReplacement try engine.editor.setGlobalScope(key: "fill/change", value: .defer) try engine.block.setScopeEnabled(videoClip, key: "fill/change", enabled: true) ``` ### Overlay Layout Adjustments Enable layout scopes only for blocks that users may reposition. The sample allows moving, resizing, and rotating the title overlay while keeping the video clip layout fixed. ```swift highlight-lockVideoDesign-layoutAdjustments try engine.editor.setGlobalScope(key: "layer/move", value: .defer) try engine.editor.setGlobalScope(key: "layer/resize", value: .defer) try engine.editor.setGlobalScope(key: "layer/rotate", value: .defer) try engine.block.setScopeEnabled(titleOverlay, key: "layer/move", enabled: true) try engine.block.setScopeEnabled(titleOverlay, key: "layer/resize", enabled: true) try engine.block.setScopeEnabled(titleOverlay, key: "layer/rotate", enabled: true) ``` ### Protected Overlays Keep scopes disabled for watermarks, legal text, brand marks, or other protected overlays. Explicitly disabling the relevant block-level scopes makes the intent clear when the matching global scopes are deferred elsewhere. ```swift highlight-lockVideoDesign-protectOverlay let lockedOverlayScopes = [ "text/edit", "text/character", "fill/change", "layer/move", "layer/resize", "layer/rotate", ] for scope in lockedOverlayScopes { try engine.block.setScopeEnabled(watermarkOverlay, key: scope, enabled: false) } ``` ## Check Effective Permissions Use `engine.block.isAllowedByScope(_:key:)` to verify what the current scope configuration actually permits. This method evaluates both global and block-level settings. ```swift highlight-lockVideoDesign-checkPermissions let canSelectVideoClip = try engine.block.isAllowedByScope(videoClip, key: "editor/select") let canReplaceVideoClip = try engine.block.isAllowedByScope(videoClip, key: "fill/change") let canMoveVideoClip = try engine.block.isAllowedByScope(videoClip, key: "layer/move") let canEditTitle = try engine.block.isAllowedByScope(titleOverlay, key: "text/edit") let canMoveTitle = try engine.block.isAllowedByScope(titleOverlay, key: "layer/move") let canSelectWatermark = try engine.block.isAllowedByScope(watermarkOverlay, key: "editor/select") let titleTextEditEnabled = try engine.block.isScopeEnabled(titleOverlay, key: "text/edit") let textEditGlobalScope = try engine.editor.getGlobalScope(key: "text/edit") print("Permission status:") print("- Can select video clip:", canSelectVideoClip) // true print("- Can replace video clip fill:", canReplaceVideoClip) // true print("- Can move video clip:", canMoveVideoClip) // false print("- Can edit title:", canEditTitle) // true print("- Can move title:", canMoveTitle) // true print("- Can select watermark:", canSelectWatermark) // false print("- Title text/edit block scope enabled:", titleTextEditEnabled) // true print("- text/edit global is .defer:", textEditGlobalScope == .defer) // true ``` The distinction between checking methods is: - `isAllowedByScope(_:key:)` returns the **effective permission** after evaluating both levels - `isScopeEnabled(_:key:)` returns only the **block-level setting** - `getGlobalScope(key:)` returns only the **global setting** ## Discover Available Scopes Use `engine.editor.findAllScopes()` instead of hardcoding a complete scope list. This keeps locking code aligned with the scopes available in the current engine. ```swift highlight-lockVideoDesign-discoverScopes let availableScopes = engine.editor.findAllScopes() print("Available scopes:", availableScopes) for scope in availableScopes { let globalSetting = try engine.editor.getGlobalScope(key: scope) print("- \(scope) is .defer:", globalSetting == .defer) } ``` ## Available Scopes Reference | Scope | Description | | ------------------------ | ---------------------------------------- | | `layer/move` | Move block position | | `layer/resize` | Resize block dimensions | | `layer/rotate` | Rotate block | | `layer/flip` | Flip block horizontally or vertically | | `layer/crop` | Crop block content | | `layer/opacity` | Change block opacity | | `layer/blendMode` | Change blend mode | | `layer/visibility` | Toggle block visibility | | `layer/clipping` | Change clipping behavior | | `fill/change` | Change fill content or text color | | `fill/changeType` | Change fill type | | `stroke/change` | Change stroke properties | | `shape/change` | Change shape type | | `text/edit` | Edit text content | | `text/character` | Change text styling such as font or size | | `appearance/adjustments` | Change color adjustments | | `appearance/filter` | Apply or change filters | | `appearance/effect` | Apply or change effects | | `appearance/blur` | Apply or change blur | | `appearance/shadow` | Apply or change shadows | | `appearance/animation` | Apply or change animations | | `lifecycle/destroy` | Delete the block | | `lifecycle/duplicate` | Duplicate the block | | `editor/add` | Add new blocks | | `editor/select` | Select blocks | ## Troubleshooting | Issue | Cause | Solution | | ---------------------------------- | ------------------------------------ | ------------------------------------------------------------------ | | Block is still editable | The global scope is `.allow` | Set the global scope to `.deny` or `.defer` | | Block is unexpectedly locked | The global scope is `.deny` | Set the global scope to `.defer` and enable the block-level scope | | Users cannot select a block | `editor/select` is still locked | Enable `editor/select` for blocks users should select | | Permission check returns `false` | The code checks the wrong scope level | Use `isAllowedByScope(_:key:)` for the effective permission | | New scopes are not locked | The code uses a hardcoded scope list | Use `findAllScopes()` to discover scopes dynamically | ## API Reference | Method | Purpose | | ------ | ------- | | `engine.editor.findAllScopes()` | Get all available scope names | | `engine.editor.setGlobalScope(key:value:)` | Set a global scope to `.allow`, `.deny`, or `.defer` | | `engine.editor.getGlobalScope(key:)` | Get the current global setting for one scope | | `engine.block.setScopeEnabled(_:key:enabled:)` | Enable or disable a scope on one block | | `engine.block.isScopeEnabled(_:key:)` | Check only the block-level scope setting | | `engine.block.isAllowedByScope(_:key:)` | Check the effective permission after global and block-level scopes are evaluated | ## Next Steps - [Lock Content](https://img.ly/docs/cesdk/mac-catalyst/rules/lock-content-9fa727/) — Lock design elements to prevent unwanted modifications using CE.SDK's scope-based permission system. - [Lock Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) - Lock templates for consistent reuse - [Rules Overview](https://img.ly/docs/cesdk/mac-catalyst/rules/overview-e27832/) - Understand the broader rules system --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Markers and Frame Rate" description: "Set the frame rate of a scene and name points and spans on its timeline with markers." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-video/markers-and-frame-rate-517cc2/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Markers and Frame Rate](https://img.ly/docs/cesdk/mac-catalyst/create-video/markers-and-frame-rate-517cc2/) --- ```swift file=@cesdk_swift_examples/engine-guides-markers-and-frame-rate/MarkersAndFrameRate.swift reference-only import Foundation import IMGLYEngine struct MarkersAndFrameRateGuideResult { let defaultFrameRate: Float let frameRate: Float let markerNames: [String] let playbackTimeAtBeat: Double let markerCountAfterRemove: Int let frameRateErrorCode: EngineErrorCode? let markerTimeErrorCode: EngineErrorCode? let markerIndexErrorCode: EngineErrorCode? let pngData: Data } @MainActor func markersAndFrameRate(engine: Engine) async throws -> MarkersAndFrameRateGuideResult { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) try engine.block.setDuration(page, duration: 10) // Creates a ball that crosses the page in 10 seconds func createBall() throws { let ball = try engine.block.create(.graphic) try engine.block.setShape(ball, shape: engine.block.createShape(.ellipse)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) try engine.block.setFill(ball, fill: fill) try engine.block.setWidth(ball, value: 160) try engine.block.setHeight(ball, value: 160) try engine.block.setPositionY(ball, value: 460) try engine.block.appendChild(to: page, child: ball) try engine.block.setDuration(ball, duration: 10) try engine.block.setKeyframes(ball, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(100)), Keyframe(time: 1, value: .float(1660)), ]) } // A ball crosses the page over the 10 seconds of the page try createBall() // 30 frames per second by default let defaultFrameRate = try engine.block.getFloat(scene, property: "scene/frameRate") try engine.block.setFloat(scene, property: "scene/frameRate", value: 25) let frameRate = try engine.block.getFloat(scene, property: "scene/frameRate") print(defaultFrameRate, frameRate) // 30.0 25.0 // A marker has a name, a start time and a duration in seconds. // A duration of 0 marks a point. try engine.scene.addMarker(name: "intro", time: 0, duration: 2.5) try engine.scene.addMarker(name: "beat", time: 4) try engine.scene.addMarker(name: "beat", time: 6) try engine.scene.addMarker(name: "outro", time: 8, duration: 2) let markers = try engine.scene.getMarkers() let markerNames = markers.map(\.name) print(markerNames) // ["intro", "beat", "beat", "outro"] // Jump to the first marker named "beat" if let beat = markers.first(where: { $0.name == "beat" }) { try engine.block.setPlaybackTime(page, time: beat.time) } let playbackTimeAtBeat = try engine.block.getPlaybackTime(page) print(playbackTimeAtBeat) // 4.0 // Remove the second "beat" by its index in the list try engine.scene.removeMarker(at: 2) let markerCountAfterRemove = try engine.scene.getMarkers().count print(markerCountAfterRemove) // 3 var frameRateErrorCode: EngineErrorCode? do { // A frame rate is greater than 0 and at most 240 try engine.block.setFloat(scene, property: "scene/frameRate", value: 0) } catch let error as EngineError { frameRateErrorCode = error.catalogCode print(frameRateErrorCode == .sceneFrameRateOutOfRange) // true } var markerTimeErrorCode: EngineErrorCode? do { try engine.scene.addMarker(name: "before", time: -1) } catch let error as EngineError { markerTimeErrorCode = error.catalogCode print(markerTimeErrorCode == .sceneMarkerInvalidTime) // true } var markerIndexErrorCode: EngineErrorCode? do { try engine.scene.removeMarker(at: 10) } catch let error as EngineError { markerIndexErrorCode = error.catalogCode print(markerIndexErrorCode == .sceneMarkerIndexOutOfRange) // true } // The playback time is at the "beat" marker, 4 seconds into the page let pngData = try await engine.block.export(page, mimeType: .png) print(!pngData.isEmpty) // true try await engine.captureGuide(data: pngData, label: "hero", mimeType: .png) return MarkersAndFrameRateGuideResult( defaultFrameRate: defaultFrameRate, frameRate: frameRate, markerNames: markerNames, playbackTimeAtBeat: playbackTimeAtBeat, markerCountAfterRemove: markerCountAfterRemove, frameRateErrorCode: frameRateErrorCode, markerTimeErrorCode: markerTimeErrorCode, markerIndexErrorCode: markerIndexErrorCode, pngData: pngData, ) } ``` Set the frame rate of a scene and name points and spans on its timeline with markers. ![A ball 40 percent of its way across the page, at the beat marker at 4 seconds](./assets/swift-based.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-markers-and-frame-rate) > **Caution:** Experimental: these APIs may change in a minor release. > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. The frame rate of a scene sets how many frames a second of its timeline has. Markers give names to points and spans on that timeline, such as chapters or beats, and travel with the scene. The editor has no controls for either, so you set them with the API. This guide covers how to set the frame rate, how to add, read and remove markers, how to jump to a marker, and the errors to expect. It ends with a frame export. ## Creating the Ball The example works on `scene` and on `page`, a page of 10 seconds. A small local function adds a ball that crosses the page in 10 seconds, so the playback time is easy to see. ```swift highlight-markersAndFrameRate-createBall // Creates a ball that crosses the page in 10 seconds func createBall() throws { let ball = try engine.block.create(.graphic) try engine.block.setShape(ball, shape: engine.block.createShape(.ellipse)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1)) try engine.block.setFill(ball, fill: fill) try engine.block.setWidth(ball, value: 160) try engine.block.setHeight(ball, value: 160) try engine.block.setPositionY(ball, value: 460) try engine.block.appendChild(to: page, child: ball) try engine.block.setDuration(ball, duration: 10) try engine.block.setKeyframes(ball, property: "position/x", keyframes: [ Keyframe(time: 0, value: .float(100)), Keyframe(time: 1, value: .float(1660)), ]) } ``` The example calls it once. ```swift highlight-markersAndFrameRate-ball // A ball crosses the page over the 10 seconds of the page try createBall() ``` ## Setting the Frame Rate `scene/frameRate` is a number property of the scene, in frames per second. It is 30 by default and accepts values greater than 0 and up to 240. `getFloat(_:property:)` reads it and `setFloat(_:property:value:)` sets it. The preview motion blur uses it, and so does a video export that passes no frame rate of its own. ```swift highlight-markersAndFrameRate-frameRate // 30 frames per second by default let defaultFrameRate = try engine.block.getFloat(scene, property: "scene/frameRate") try engine.block.setFloat(scene, property: "scene/frameRate", value: 25) let frameRate = try engine.block.getFloat(scene, property: "scene/frameRate") print(defaultFrameRate, frameRate) // 30.0 25.0 ``` ## Adding Markers Markers name points or spans on the scene timeline. `engine.scene.addMarker(name:time:duration:)` appends a marker with a name, a start time and a duration in seconds. A duration of 0, the default, marks a point. Several markers may share a name. ```swift highlight-markersAndFrameRate-addMarkers // A marker has a name, a start time and a duration in seconds. // A duration of 0 marks a point. try engine.scene.addMarker(name: "intro", time: 0, duration: 2.5) try engine.scene.addMarker(name: "beat", time: 4) try engine.scene.addMarker(name: "beat", time: 6) try engine.scene.addMarker(name: "outro", time: 8, duration: 2) ``` ## Reading Markers `engine.scene.getMarkers()` returns the markers in the order they were added, as `SceneMarker` values with a `name`, a `time` and a `duration`. ```swift highlight-markersAndFrameRate-readMarkers let markers = try engine.scene.getMarkers() let markerNames = markers.map(\.name) print(markerNames) // ["intro", "beat", "beat", "outro"] ``` ## Jumping to a Marker Markers do not change the playback on their own. To jump to one, find it by name and pass its time to `setPlaybackTime(_:time:)` of the page. ```swift highlight-markersAndFrameRate-seek // Jump to the first marker named "beat" if let beat = markers.first(where: { $0.name == "beat" }) { try engine.block.setPlaybackTime(page, time: beat.time) } let playbackTimeAtBeat = try engine.block.getPlaybackTime(page) print(playbackTimeAtBeat) // 4.0 ``` ## Removing a Marker `engine.scene.removeMarker(at:)` removes a marker by its index in the list that `engine.scene.getMarkers()` returns. The markers after it move up by one. ```swift highlight-markersAndFrameRate-removeMarker // Remove the second "beat" by its index in the list try engine.scene.removeMarker(at: 2) let markerCountAfterRemove = try engine.scene.getMarkers().count print(markerCountAfterRemove) // 3 ``` ## Handling Errors A finite frame rate outside its range, a negative or non-finite marker time or duration, and an index outside the marker list are refused, and nothing changes. The call throws an `EngineError` whose `catalogCode` names the cause. ```swift highlight-markersAndFrameRate-errors var frameRateErrorCode: EngineErrorCode? do { // A frame rate is greater than 0 and at most 240 try engine.block.setFloat(scene, property: "scene/frameRate", value: 0) } catch let error as EngineError { frameRateErrorCode = error.catalogCode print(frameRateErrorCode == .sceneFrameRateOutOfRange) // true } var markerTimeErrorCode: EngineErrorCode? do { try engine.scene.addMarker(name: "before", time: -1) } catch let error as EngineError { markerTimeErrorCode = error.catalogCode print(markerTimeErrorCode == .sceneMarkerInvalidTime) // true } var markerIndexErrorCode: EngineErrorCode? do { try engine.scene.removeMarker(at: 10) } catch let error as EngineError { markerIndexErrorCode = error.catalogCode print(markerIndexErrorCode == .sceneMarkerIndexOutOfRange) // true } ``` ## Exporting a Frame The playback still stands at the `beat` marker, so we export the page as PNG at 4 seconds, with the ball 40 percent of its way across the page. ```swift highlight-markersAndFrameRate-export // The playback time is at the "beat" marker, 4 seconds into the page let pngData = try await engine.block.export(page, mimeType: .png) print(!pngData.isEmpty) // true ``` ## Markers, Frame Rate and Other Operations - Saving and loading keep the markers and the frame rate. Undo and redo restore edits to them. - A scene load drops markers with a negative time or duration, and loads a frame rate above 240 as 240. - A scene imported from a Lottie file on Web or Node carries the Lottie frame rate and markers, so they are already set when you load that scene. ## Troubleshooting - **The frame rate is not a number** (`EngineErrorCode.blockValueNotFinite`): pass a finite value. - **The frame rate is refused** (`EngineErrorCode.sceneFrameRateOutOfRange`): pass a value greater than 0 and up to 240. - **The marker is refused** (`EngineErrorCode.sceneMarkerInvalidTime`): pass a time and a duration that are finite and not negative. - **The marker cannot be removed** (`EngineErrorCode.sceneMarkerIndexOutOfRange`): pass an index from 0 to the number of markers minus 1. - **The wrong marker is removed**: indexes follow the order of the list, and removing a marker moves the later ones up. Read the markers again after you remove one. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.getFloat(_:property:)` | Read the frame rate of the scene | | `engine.block.setFloat(_:property:value:)` | Set the frame rate, greater than 0 and up to 240 | | `engine.scene.addMarker(name:time:duration:)` | Add a marker; `duration` defaults to 0 | | `engine.scene.getMarkers()` | Read the markers in the order they were added | | `engine.scene.removeMarker(at:)` | Remove the marker at an index | | `engine.block.setPlaybackTime(_:time:)` | Jump to a time, for example a marker's | | `engine.block.getPlaybackTime(_:)` | Read the playback time | | `engine.block.create(_:)` | Create the ball | | `engine.block.createShape(_:)` | Create the ellipse shape | | `engine.block.setShape(_:shape:)` | Give the ball its shape | | `engine.block.createFill(_:)` | Create a color fill | | `engine.block.setColor(_:property:color:)` | Set the fill color | | `engine.block.setFill(_:fill:)` | Give the ball its fill | | `engine.block.setWidth(_:value:)` | Set the width of the ball | | `engine.block.setHeight(_:value:)` | Set the height of the ball | | `engine.block.setPositionY(_:value:)` | Set the y position of the ball | | `engine.block.appendChild(to:child:)` | Add the ball to the page | | `engine.block.setDuration(_:duration:)` | Set the duration of the ball | | `engine.block.setKeyframes(_:property:keyframes:mode:)` | Move the ball | | `engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:)` | Export the frame as PNG | ### Properties | Property | Type | Description | | --- | --- | --- | | `scene/frameRate` | Float | Frames per second of the scene, greater than 0 and up to 240. Default 30 | ## Next Steps - [Scenes](https://img.ly/docs/cesdk/mac-catalyst/concepts/scenes-e8596d/): the other properties and methods of a scene. - [Motion Blur](https://img.ly/docs/cesdk/mac-catalyst/animation/motion-blur-122f43/): blur moving blocks over one frame of the scene frame rate. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create Videos Overview" description: "Learn how Swift video projects work in CE.SDK and choose the right guide for UI-based or programmatic video workflows." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-video/overview-b06512/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/create-video/overview-b06512/) --- Understand Swift video projects in CE.SDK before choosing an editor UI, Engine API workflow, or focused implementation guide. CE.SDK video projects add time-based editing to the same scene and block model used for static designs. A video scene can contain pages, timeline tracks, media-backed clips, overlays, captions, audio, and export settings. Use this overview to decide where your Swift integration should start. Start with the [Video Editor Starter Kit](#broken-link-e1nlor) for iOS when you need an interactive timeline UI. Use Engine APIs for automation, custom controls, template-driven output, or scene preparation that runs before the editor opens. ## Core Video Concepts Video scenes are timeline-based scenes. Each page defines an independent timeline and the output frame size for that timeline. Blocks placed on the page or inside tracks become active for a duration, and the page playback time determines which frame is visible. Scenes own the time-based project and shared resources. Pages define timelines and frame sizes. Clips are timed blocks, often backed by video media. Tracks group clips and can sequence them over time. Audio can come from video media or from standalone audio blocks. Export turns the page timeline into a shareable video output. Timeline values are measured in seconds. Duration controls how long a block or media source is active, time offset controls when it starts in its parent timeline, and trim values control which part of a media source plays. These concepts show up across the focused Swift video guides. ## UI-Based Editing Reach for the CE.SDK editor UI when users need to assemble or adjust videos interactively. On iOS, the editor UI covers common editing tasks such as arranging clips on a timeline, trimming media, adding overlays, editing captions, placing watermarks, previewing playback, and balancing audio. Apps usually customize that UI around their product workflow: available tools, asset sources, export actions, brand controls, permissions, and app-specific navigation. Keep those choices in your editor configuration, then use Engine APIs for any scene preparation or post-processing that should happen before or after the user edits. ## Programmatic Editing Use Engine APIs when your app needs reproducible video output or custom automation. Programmatic workflows can create video scenes, add pages and tracks, load media, arrange clips, set durations and offsets, adjust trim ranges, control audio, generate previews, and export finished pages. For step-by-step code, continue with the focused guides for timeline editing, joining and arranging clips, trimming, captions, watermarks, and export. ## Platform Support and Constraints Swift video workflows run on the user's Apple device and depend on platform media, rendering, storage, and codec support. Test the codecs, resolutions, frame rates, memory usage, and export settings that match your target devices, especially for long videos, large source files, or high-resolution output. Some behavior is platform-specific. Export options, media loading, file access, permissions, and available codecs differ across platforms. Reach for the Swift implementation guides when a workflow touches local file access, asset loading, playback performance, or export configuration. ## Audio in Video Projects Video projects can include audio embedded in video media and standalone audio blocks. Common Swift workflows include muting audio embedded in video fills, adding background music, adding voiceover, placing sound effects on the timeline, and adjusting volume across multiple sources. Standalone audio blocks use the same timeline concepts as visual blocks: duration determines how long the audio plays, time offset determines when it starts, and trim values can select the source-media segment. Include audio in a rendered video by keeping those audio sources on the page and exporting the page as video. ## Export and Output Export turns a page timeline into an output artifact. Swift video export uses Engine video export APIs to render a page range and return encoded video content. Export options can control details such as frame rate, bitrate, target dimensions, and H.264 settings. The right export setup depends on the product: a social video may prioritize size and speed, while a template workflow may prioritize resolution and repeatable output. Use dedicated export guides for format, compression, progress, storage, and file-handling details. ## AI and App-Specific Workflows AI-assisted and app-specific video features fit around the same scene model. Your app can generate scripts, suggest edits, create captions, prepare assets, or call external services before applying the result to a CE.SDK scene. Treat those workflows as integrations unless a focused CE.SDK guide documents them as built-in platform behavior. ## Next Steps - [Video Editor Starter Kit](#broken-link-e1nlor) — For iOS, start from the timeline UI for interactive editing. - [Programmatic Editing](https://img.ly/docs/cesdk/mac-catalyst/edit-video/programmatic-8429af/) — Edit video scenes with CE.SDK Engine APIs on Swift. - [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) — Arrange clips, audio, overlays, and timeline previews. - [Join and Arrange Video Clips](https://img.ly/docs/cesdk/mac-catalyst/edit-video/join-and-arrange-3bbc30/) — Combine clips into sequences and organize them on tracks. - [Control Audio and Video](https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/) — Play, pause, seek, and preview audio or video content. - [Add Captions](https://img.ly/docs/cesdk/mac-catalyst/edit-video/add-captions-f67565/) — Add synchronized captions to Swift video scenes. - [Add Watermark](https://img.ly/docs/cesdk/mac-catalyst/edit-video/add-watermark-762ce6/) — Add text or image watermarks to exported videos. - [Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) — Render output for sharing or publishing. - [Trim](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) — Control which portion of source media plays --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Programmatic Creation" description: "Create and export video scenes entirely through code with the CE.SDK Engine on iOS, macOS, and Mac Catalyst." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-video/programmatic-2b243c/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Programmatic Creation](https://img.ly/docs/cesdk/mac-catalyst/create-video/programmatic-2b243c/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-video-programmatic/CreateVideoProgrammatic.swift reference-only import Foundation import IMGLYEngine @MainActor func createVideoProgrammatic(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.appendChild(to: scene, child: page) let baseURL = try engine.guidesBaseURL let introURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) let detailURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-kampus-production-8154913.mp4", ) let introClip = try makeVideoClip(engine: engine, videoURL: introURL) let detailClip = try makeVideoClip(engine: engine, videoURL: detailURL) let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) try engine.block.appendChild(to: track, child: introClip.block) try engine.block.appendChild(to: track, child: detailClip.block) try engine.block.fillParent(track) // Keep the guide export short; use the clip length your app needs. let sampleClipDurationSeconds = 2.0 try await engine.block.forceLoadAVResource(introClip.fill) let introSource = try engine.block.getAVResourceTotalDuration(introClip.fill) let introDuration = min(sampleClipDurationSeconds, introSource) try engine.block.setDuration(introClip.block, duration: introDuration) try await engine.block.forceLoadAVResource(detailClip.fill) let detailSource = try engine.block.getAVResourceTotalDuration(detailClip.fill) let detailDuration = min(sampleClipDurationSeconds, detailSource) try engine.block.setDuration(detailClip.block, duration: detailDuration) let pageDuration = introDuration + detailDuration try engine.block.setDuration(page, duration: pageDuration) // Export a compact preview file; use your delivery size and frame rate in production. // videoBitrate: .auto derives a bounded bitrate from the resolution/framerate. It is // recommended over the default .system mode. Pass .custom(bitsPerSecond) instead // for an explicit bitrate. let exportOptions = VideoExportOptions( videoBitrate: .auto, framerate: 15, targetWidth: 640, targetHeight: 360, ) let exportStream = try await engine.block.exportVideo( page, mimeType: .mp4, options: exportOptions, ) var videoData: Blob? for try await event in exportStream { switch event { case let .progress(rendered, encoded, total): let percent = total > 0 ? Int(Double(encoded) / Double(total) * 100) : 0 print("Export \(percent)% — encoded \(encoded)/\(total) (rendered \(rendered))") case let .finished(video: blob): videoData = blob } } guard let videoBytes = videoData else { throw NSError( domain: "ly.img.guide", code: 0, userInfo: [NSLocalizedDescriptionKey: "exportVideo finished without a video blob."], ) } let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("programmatic-video.mp4") try videoBytes.write(to: outputURL) assert(!videoBytes.isEmpty) let writtenSize = try outputURL.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0 assert(writtenSize > 0) } private struct VideoClip { let block: DesignBlockID let fill: DesignBlockID } @MainActor private func makeVideoClip( engine: Engine, videoURL: URL, ) throws -> VideoClip { let clip = try engine.block.create(.graphic) try engine.block.setShape(clip, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) // Video fills read their media source from this Engine property key. try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(clip, fill: videoFill) return VideoClip(block: clip, fill: videoFill) } @MainActor func createSingleSourceVideoScene(engine: Engine) async throws -> DesignBlockID { let videoURL = try engine.guidesBaseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) return try await engine.scene.create(fromVideo: videoURL) } ``` Create a video scene entirely through code, add clips to a track, set durations, and export the result as an MP4 file. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-video-programmatic) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. CE.SDK video scenes can be built without opening the editor UI. This is useful for automation, template-driven rendering, and app flows that create media from known inputs. This guide uses the Swift Engine API to create a video scene, arrange clips on a track, load media metadata, set durations, and export the page as MP4. Use [Join and Arrange Video Clips](https://img.ly/docs/cesdk/mac-catalyst/edit-video/join-and-arrange-3bbc30/) when you need multi-clip reordering, time offsets, or overlay tracks after the initial scene is created. ## Create a Video Scene Create a timeline-enabled scene with `engine.scene.createVideo()`. A page holds the video composition and defines the canvas dimensions. ```swift highlight-createVideoProgrammatic-create-scene let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.appendChild(to: scene, child: page) ``` For a one-source video scene, call `engine.scene.create(fromVideo:)` with the video URL. The main sample uses `createVideo()` because it shows tracks, multiple clips, and export timing. ```swift highlight-createVideoProgrammatic-create-from-video @MainActor func createSingleSourceVideoScene(engine: Engine) async throws -> DesignBlockID { let videoURL = try engine.guidesBaseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) return try await engine.scene.create(fromVideo: videoURL) } ``` ## Add Video Clips Each clip is a graphic block with a rectangular shape and a video fill. The helper returns both handles because later timing APIs operate on the graphic block, while media metadata APIs operate on the video fill. ```swift highlight-createVideoProgrammatic-create-video-clip-helper private struct VideoClip { let block: DesignBlockID let fill: DesignBlockID } @MainActor private func makeVideoClip( engine: Engine, videoURL: URL, ) throws -> VideoClip { let clip = try engine.block.create(.graphic) try engine.block.setShape(clip, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) // Video fills read their media source from this Engine property key. try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(clip, fill: videoFill) return VideoClip(block: clip, fill: videoFill) } ``` Create the clips from source URLs: ```swift highlight-createVideoProgrammatic-add-video-clips let introClip = try makeVideoClip(engine: engine, videoURL: introURL) let detailClip = try makeVideoClip(engine: engine, videoURL: detailURL) ``` ## Arrange Clips on a Track Append the clips to a `.track` block in playback order. The track sequences its children from their durations, and `fillParent(track)` sizes the track block to its parent page. For groups and tracks, `fillParent(...)` also fills child blocks against the nearest parent that is not a group or track, so the unsized clip graphics in this sample fill the page frame before the track is sized. ```swift highlight-createVideoProgrammatic-arrange-track let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) try engine.block.appendChild(to: track, child: introClip.block) try engine.block.appendChild(to: track, child: detailClip.block) try engine.block.fillParent(track) ``` ## Load Media and Set Durations Load each video resource before reading metadata. Duration values use seconds. ```swift highlight-createVideoProgrammatic-load-media-and-timing // Keep the guide export short; use the clip length your app needs. let sampleClipDurationSeconds = 2.0 try await engine.block.forceLoadAVResource(introClip.fill) let introSource = try engine.block.getAVResourceTotalDuration(introClip.fill) let introDuration = min(sampleClipDurationSeconds, introSource) try engine.block.setDuration(introClip.block, duration: introDuration) try await engine.block.forceLoadAVResource(detailClip.fill) let detailSource = try engine.block.getAVResourceTotalDuration(detailClip.fill) let detailDuration = min(sampleClipDurationSeconds, detailSource) try engine.block.setDuration(detailClip.block, duration: detailDuration) let pageDuration = introDuration + detailDuration try engine.block.setDuration(page, duration: pageDuration) ``` The sample derives safe clip durations from the source media duration and sets the page duration to the combined clip length. ## Export the Video Export the page with `engine.block.exportVideo(_:mimeType:options:)`. The call returns an `AsyncThrowingStream` that yields `.progress` events during encoding and a final `.finished(video:)` event carrying the encoded `Blob`. The sample exports MP4, reports progress, and passes a smaller target resolution and frame rate to produce a compact verification file while keeping the page at 1280x720. It also selects `VideoBitrate.auto`, a bounded bitrate derived from the resolution and frame rate. ```swift highlight-createVideoProgrammatic-export-video // Export a compact preview file; use your delivery size and frame rate in production. // videoBitrate: .auto derives a bounded bitrate from the resolution/framerate. It is // recommended over the default .system mode. Pass .custom(bitsPerSecond) instead // for an explicit bitrate. let exportOptions = VideoExportOptions( videoBitrate: .auto, framerate: 15, targetWidth: 640, targetHeight: 360, ) let exportStream = try await engine.block.exportVideo( page, mimeType: .mp4, options: exportOptions, ) var videoData: Blob? for try await event in exportStream { switch event { case let .progress(rendered, encoded, total): let percent = total > 0 ? Int(Double(encoded) / Double(total) * 100) : 0 print("Export \(percent)% — encoded \(encoded)/\(total) (rendered \(rendered))") case let .finished(video: blob): videoData = blob } } guard let videoBytes = videoData else { throw NSError( domain: "ly.img.guide", code: 0, userInfo: [NSLocalizedDescriptionKey: "exportVideo finished without a video blob."], ) } ``` Write the returned `Blob` to disk with `Blob.write(to:)`. ```swift highlight-createVideoProgrammatic-write-file let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("programmatic-video.mp4") try videoBytes.write(to: outputURL) ``` > **Note:** On iPhone and iPad apps, the export process is automatically suspended when the > app moves to the background and resumed when it returns to the foreground. This > automatic handling does not apply to Mac Catalyst or macOS targets. ## API Reference | API | Category | Purpose | | --- | --- | --- | | `engine.scene.createVideo()` | Scene | Create an empty video scene with timeline support. | | `engine.scene.create(fromVideo:)` | Scene | Create a one-source video scene from a URL. | | `engine.block.create(_:)` | Block | Create pages, tracks, graphics, and other blocks. | | `engine.block.createShape(_:)` | Shape | Create a shape for a graphic block. | | `engine.block.setShape(_:shape:)` | Shape | Assign a shape to a graphic block. | | `engine.block.createFill(_:)` | Fill | Create a video fill. | | `engine.block.setFill(_:fill:)` | Fill | Assign a fill to a block. | | `engine.block.setString(_:property:value:)` | Fill | Set the source URI on a video fill via the `fill/video/fileURI` property. | | `engine.block.appendChild(to:child:)` | Hierarchy | Attach scene, page, track, and clip blocks. | | `engine.block.fillParent(_:)` | Layout | Resize and position the passed block to fill its parent; for groups and tracks, fill child blocks against the nearest non-group/non-track parent first. | | `engine.block.setWidth(_:value:)` / `setHeight(_:value:)` | Layout | Set the page's natural dimensions. | | `engine.block.setDuration(_:duration:)` | Timing | Set clip or page duration in seconds. | | `engine.block.getDuration(_:)` | Timing | Read a block's duration in seconds. | | `engine.block.forceLoadAVResource(_:)` | Media | Load a video fill before reading metadata. | | `engine.block.getAVResourceTotalDuration(_:)` | Media | Read the source media duration in seconds. | | `engine.block.exportVideo(_:mimeType:options:)` | Export | Export a page timeline as an `AsyncThrowingStream` that yields progress events and a final encoded `Blob`. | | `VideoExportOptions(framerate:targetWidth:targetHeight:videoBitrate:...)` | Export | Configure framerate, output dimensions, and bitrate for the export. | ## Troubleshooting - **Engine reference is unavailable**: Initialize CE.SDK and obtain an `Engine` instance before running the scene creation code; the Engine reference note above links to the setup flow. - **Clip not visible**: Verify that the graphic block has a shape and fill, and that it is appended to a track or page. - **Source duration is zero**: Call `forceLoadAVResource(_:)` on the video fill before metadata APIs. - **Export is empty**: Set a positive page duration and confirm each clip has a non-zero duration before exporting. - **Remote media does not load**: Check that the URL is reachable and that the device runtime supports the media format. ## Next Steps - [Create Videos Overview](https://img.ly/docs/cesdk/mac-catalyst/create-video/overview-b06512/) - Understand video scenes and time-based editing - [Join and Arrange Video Clips](https://img.ly/docs/cesdk/mac-catalyst/edit-video/join-and-arrange-3bbc30/) - Combine multiple video clips into sequences and organize them on the timeline using tracks and time offsets in CE.SDK. - [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) - Build interactive video timelines - [Trim](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) - Control which portion of source media plays - [Control Audio and Video](https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/) - Configure playback, trim, and resource control - [Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) - Export images, videos, and other output formats --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Timeline Editor" description: "Use the timeline editor to arrange and edit video clips, audio, and animations frame by frame." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) --- ```swift file=@cesdk_swift_examples/engine-guides-timeline-editor/TimelineEditor.swift reference-only import Foundation import IMGLYEngine @MainActor func timelineEditor(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let primaryURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) let overlayURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-kampus-production-8154913.mp4", ) let audioURL = baseURL.appendingPathComponent("ly.img.audio/audios/far_from_home.m4a") let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 10) let primaryTrack = try engine.block.create(.track) let overlayTrack = try engine.block.create(.track) let audioTrack = try engine.block.create(.track) try engine.block.appendChild(to: page, child: primaryTrack) try engine.block.appendChild(to: page, child: overlayTrack) try engine.block.appendChild(to: page, child: audioTrack) try engine.block.setBool(overlayTrack, property: "track/automaticallyManageBlockOffsets", value: false) let primaryClip = try engine.block.create(.graphic) try engine.block.setShape(primaryClip, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(primaryClip, value: 0) try engine.block.setPositionY(primaryClip, value: 0) try engine.block.setWidth(primaryClip, value: 1280) try engine.block.setHeight(primaryClip, value: 720) let primaryFill = try engine.block.createFill(.video) try engine.block.setURL(primaryFill, property: "fill/video/fileURI", value: primaryURL) try engine.block.setFill(primaryClip, fill: primaryFill) try engine.block.appendChild(to: primaryTrack, child: primaryClip) let overlayClip = try engine.block.create(.graphic) try engine.block.setShape(overlayClip, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(overlayClip, value: 820) try engine.block.setPositionY(overlayClip, value: 80) try engine.block.setWidth(overlayClip, value: 360) try engine.block.setHeight(overlayClip, value: 220) let overlayFill = try engine.block.createFill(.video) try engine.block.setURL(overlayFill, property: "fill/video/fileURI", value: overlayURL) try engine.block.setFill(overlayClip, fill: overlayFill) try engine.block.appendChild(to: overlayTrack, child: overlayClip) let audioClip = try engine.block.create(.audio) try engine.block.setURL(audioClip, property: "audio/fileURI", value: audioURL) try engine.block.appendChild(to: audioTrack, child: audioClip) try await engine.block.forceLoadAVResource(primaryFill) try await engine.block.forceLoadAVResource(overlayFill) try await engine.block.forceLoadAVResource(audioClip) try engine.block.setDuration(primaryClip, duration: 8) try engine.block.setTrimOffset(primaryFill, offset: 2) try engine.block.setTrimLength(primaryFill, length: 8) try engine.block.setLooping(primaryFill, looping: false) try engine.block.setMuted(primaryFill, muted: true) try engine.block.setTimeOffset(overlayClip, offset: 3) try engine.block.setDuration(overlayClip, duration: 4) try engine.block.setTimeOffset(audioClip, offset: 0) try engine.block.setDuration(audioClip, duration: 10) try engine.block.setPlaybackTime(page, time: 3.5) let overlayVisible = try engine.block.isVisibleAtCurrentPlaybackTime(overlayClip) print("Overlay visible at 3.5s:", overlayVisible) try engine.block.setPlaying(page, enabled: true) print("Page is playing:", try engine.block.isPlaying(page)) try engine.block.setPlaying(page, enabled: false) var videoThumbnails: [VideoThumbnail] = [] for try await thumbnail in engine.block.generateVideoThumbnailSequence( primaryFill, thumbnailHeight: 72, timeRange: 0.0 ... 8.0, numberOfFrames: 4, ) { videoThumbnails.append(thumbnail) } var audioChunks: [AudioThumbnail] = [] for try await chunk in engine.block.generateAudioThumbnailSequence( audioClip, samplesPerChunk: 40, timeRange: 0.0 ... 10.0, numberOfSamples: 160, numberOfChannels: 2, ) { audioChunks.append(chunk) } print("Received \(videoThumbnails.count) video frames and \(audioChunks.count) waveform chunks") let exportsDirectory = FileManager.default.temporaryDirectory let exportStream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in exportStream { switch event { case let .progress(renderedFrames, encodedFrames, totalFrames): print("Rendered \(renderedFrames) / encoded \(encodedFrames) of \(totalFrames) frames") case let .finished(video: blob): try blob.write(to: exportsDirectory.appendingPathComponent("timeline.mp4")) } } } ``` Build video timelines with CE.SDK by arranging tracks, clips, trim ranges, playback controls, thumbnails, and MP4 export from Swift. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-timeline-editor) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Use the Engine APIs in this guide when you need to prepare a video scene programmatically, build a custom timeline surface, or automate timeline edits before opening or exporting the scene. On iOS, the [Video Editor Starter Kit](#broken-link-e1nlor) already renders a built-in timeline component in its bottom panel. ## Timeline Hierarchy CE.SDK represents video timelines through the same block hierarchy that any video-editing surface renders: ```text Scene └── Page ├── Track │ ├── Clip │ └── Clip ├── Overlay track └── Audio track ``` A page defines the composition duration, tracks group parallel lanes, and each clip controls its own duration, trim range, and time offset. ## Create a Scene Start with `engine.scene.createVideo()`, then add a page with the final frame size and duration. The page is the block you play, scrub, and export. ```swift highlight-timelineEditor-create-video-scene let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 10) ``` ## Create Tracks Tracks organize clips into timeline lanes. The primary track can keep automatic offset management so clips play one after another, while overlay tracks often disable automatic offsets so you can position clips freely in time. ```swift highlight-timelineEditor-create-tracks let primaryTrack = try engine.block.create(.track) let overlayTrack = try engine.block.create(.track) let audioTrack = try engine.block.create(.track) try engine.block.appendChild(to: page, child: primaryTrack) try engine.block.appendChild(to: page, child: overlayTrack) try engine.block.appendChild(to: page, child: audioTrack) try engine.block.setBool(overlayTrack, property: "track/automaticallyManageBlockOffsets", value: false) ``` ## Add Video and Audio Clips Video clips are graphic blocks with a video fill. Audio clips use the `.audio` block type and can live on their own track so a timeline UI can present them as an audio lane. ```swift highlight-timelineEditor-add-clips let primaryClip = try engine.block.create(.graphic) try engine.block.setShape(primaryClip, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(primaryClip, value: 0) try engine.block.setPositionY(primaryClip, value: 0) try engine.block.setWidth(primaryClip, value: 1280) try engine.block.setHeight(primaryClip, value: 720) let primaryFill = try engine.block.createFill(.video) try engine.block.setURL(primaryFill, property: "fill/video/fileURI", value: primaryURL) try engine.block.setFill(primaryClip, fill: primaryFill) try engine.block.appendChild(to: primaryTrack, child: primaryClip) let overlayClip = try engine.block.create(.graphic) try engine.block.setShape(overlayClip, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(overlayClip, value: 820) try engine.block.setPositionY(overlayClip, value: 80) try engine.block.setWidth(overlayClip, value: 360) try engine.block.setHeight(overlayClip, value: 220) let overlayFill = try engine.block.createFill(.video) try engine.block.setURL(overlayFill, property: "fill/video/fileURI", value: overlayURL) try engine.block.setFill(overlayClip, fill: overlayFill) try engine.block.appendChild(to: overlayTrack, child: overlayClip) let audioClip = try engine.block.create(.audio) try engine.block.setURL(audioClip, property: "audio/fileURI", value: audioURL) try engine.block.appendChild(to: audioTrack, child: audioClip) ``` ## Trim and Position Clips Load media resources before reading source durations or setting trim ranges. `setTrimOffset(_:offset:)` chooses where playback starts inside the source file, `setTrimLength(_:length:)` chooses how much source media is used, and `setTimeOffset(_:offset:)` places the clip on the page timeline. ```swift highlight-timelineEditor-trim-and-position try await engine.block.forceLoadAVResource(primaryFill) try await engine.block.forceLoadAVResource(overlayFill) try await engine.block.forceLoadAVResource(audioClip) try engine.block.setDuration(primaryClip, duration: 8) try engine.block.setTrimOffset(primaryFill, offset: 2) try engine.block.setTrimLength(primaryFill, length: 8) try engine.block.setLooping(primaryFill, looping: false) try engine.block.setMuted(primaryFill, muted: true) try engine.block.setTimeOffset(overlayClip, offset: 3) try engine.block.setDuration(overlayClip, duration: 4) try engine.block.setTimeOffset(audioClip, offset: 0) try engine.block.setDuration(audioClip, duration: 10) ``` In this sample, the primary video skips the first two seconds, uses an eight-second source range, and does not loop after that range. It also mutes the primary video fill so the source audio does not compete with the dedicated audio track. The overlay clip starts three seconds into the page timeline. ## Control Playback Use page playback time for scrubbing and `setPlaying(_:enabled:)` for preview playback. After seeking, `isVisibleAtCurrentPlaybackTime(_:)` lets a custom timeline or preview surface confirm whether a clip is active at the playhead, while `isPlaying(_:)` reports whether the page is currently in active playback. ```swift highlight-timelineEditor-playback try engine.block.setPlaybackTime(page, time: 3.5) let overlayVisible = try engine.block.isVisibleAtCurrentPlaybackTime(overlayClip) print("Overlay visible at 3.5s:", overlayVisible) try engine.block.setPlaying(page, enabled: true) print("Page is playing:", try engine.block.isPlaying(page)) try engine.block.setPlaying(page, enabled: false) ``` ## Generate Timeline Thumbnails Generate video thumbnails from a video fill or block and waveform data from an audio block. `generateVideoThumbnailSequence(_:thumbnailHeight:timeRange:numberOfFrames:)` returns an `AsyncThrowingStream` — each `VideoThumbnail` exposes a `CGImage` you can render directly. `generateAudioThumbnailSequence(_:samplesPerChunk:timeRange:numberOfSamples:numberOfChannels:)` returns an `AsyncThrowingStream` of channel-interleaved sample chunks that you can plot as a waveform. ```swift highlight-timelineEditor-thumbnails var videoThumbnails: [VideoThumbnail] = [] for try await thumbnail in engine.block.generateVideoThumbnailSequence( primaryFill, thumbnailHeight: 72, timeRange: 0.0 ... 8.0, numberOfFrames: 4, ) { videoThumbnails.append(thumbnail) } var audioChunks: [AudioThumbnail] = [] for try await chunk in engine.block.generateAudioThumbnailSequence( audioClip, samplesPerChunk: 40, timeRange: 0.0 ... 10.0, numberOfSamples: 160, numberOfChannels: 2, ) { audioChunks.append(chunk) } print("Received \(videoThumbnails.count) video frames and \(audioChunks.count) waveform chunks") ``` Use the emitted frame and chunk indices as stable positions in your custom timeline cache. Start a new request when the zoom range, clip trim, or visible time window changes. ## Export the Timeline Export the page with `engine.block.exportVideo(_:mimeType:options:onPreExport:uriResolver:)`. The call returns an `AsyncThrowingStream` that yields `.progress(renderedFrames:encodedFrames:totalFrames:)` while the encoder runs and `.finished(video:)` with the encoded `Data` when the export completes. Pass `MIMEType.mp4` for H.264/MP4 output and use `VideoExportOptions` to tune the H.264 profile, level, bitrate, framerate, or target dimensions. ```swift highlight-timelineEditor-export let exportsDirectory = FileManager.default.temporaryDirectory let exportStream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in exportStream { switch event { case let .progress(renderedFrames, encodedFrames, totalFrames): print("Rendered \(renderedFrames) / encoded \(encodedFrames) of \(totalFrames) frames") case let .finished(video: blob): try blob.write(to: exportsDirectory.appendingPathComponent("timeline.mp4")) } } ``` ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.createVideo()` | Creates a scene used for timeline compositions. | | `engine.block.create(_:)` | Creates pages, tracks, graphics, and audio blocks. | | `engine.block.createFill(_:)` | Creates a video fill for a graphic clip. | | `engine.block.appendChild(to:child:)` | Adds pages, tracks, and clips to the timeline hierarchy. | | `engine.block.setURL(_:property:value:)` | Sets a URL-valued property such as a fill or audio source URI. | | `engine.block.setDuration(_:duration:)` | Sets how long a page or clip is active in seconds. | | `engine.block.setTimeOffset(_:offset:)` | Places a clip on its parent timeline in seconds. | | `engine.block.forceLoadAVResource(_:)` | Loads a video fill or audio block before duration, trim, or thumbnail queries. | | `engine.block.setTrimOffset(_:offset:)` | Sets the source-media start position for playback. | | `engine.block.setTrimLength(_:length:)` | Sets the length of source media used by the clip. | | `engine.block.setLooping(_:looping:)` | Sets whether the block loops back to the beginning after reaching the end or stops. | | `engine.block.setMuted(_:muted:)` | Mutes or unmutes the audio for a block or video fill. | | `engine.block.setPlaybackTime(_:time:)` | Moves the playhead for a page or other playback-time block. | | `engine.block.setPlaying(_:enabled:)` | Starts or pauses playback for a page or media block. | | `engine.block.isVisibleAtCurrentPlaybackTime(_:)` | Returns whether a block should be visible at the current playhead position. | | `engine.block.isPlaying(_:)` | Returns whether a page or media block is currently in active playback. | | `engine.block.generateVideoThumbnailSequence(_:thumbnailHeight:timeRange:numberOfFrames:)` | Emits frame thumbnails as a `VideoThumbnail` stream for timeline strips. | | `engine.block.generateAudioThumbnailSequence(_:samplesPerChunk:timeRange:numberOfSamples:numberOfChannels:)` | Emits waveform chunks as an `AudioThumbnail` stream for audio lanes. | | `engine.block.exportVideo(_:mimeType:options:onPreExport:uriResolver:)` | Exports a page timeline as a `VideoExport` stream with optional encoder settings, background-engine setup, and URI rewriting. | ### Properties | Property | Type | Description | | --- | --- | --- | | `track/automaticallyManageBlockOffsets` | Bool | When `false`, each clip on the track keeps the time offset you assign instead of being packed after the previous clip. | | `fill/video/fileURI` | String | Source URI of the video fill. | | `audio/fileURI` | String | Source URI of the audio block. | ## Troubleshooting - **Trim calls fail:** call `forceLoadAVResource(_:)` on the video fill or audio block before setting trim offset or length. - **Clips ignore manual offsets:** set `track/automaticallyManageBlockOffsets` to `false` on tracks where clips need gaps or overlaps. - **Export is blank near the end:** make sure the page duration does not exceed the end time of the visible clips unless blank frames are intentional. - **Thumbnail generation stalls during playback:** pause playback before regenerating dense thumbnail or waveform ranges. ## Next Steps - [Trim](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) — Set trim offsets and trim lengths on video fills and audio blocks in more detail. - [Control Audio and Video](https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/) — Play, pause, seek, and preview audio and video content using playback controls and solo mode. - [Disable or Enable Features](#broken-link-f058e2) — On iOS, control which editor features are available to users by enabling or disabling them. - [Compress Exports for Smaller Files](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/compress-29105e/) — Tune format-specific compression settings to reduce export file sizes. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Update Caption Presets" description: "Extend CE.SDK video captions with custom declarative caption style presets using the engine's asset APIs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/create-video/update-caption-presets-e9c385/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Update Caption Presets](https://img.ly/docs/cesdk/mac-catalyst/create-video/update-caption-presets-e9c385/) --- ```swift file=@cesdk_swift_examples/engine-guides-update-caption-presets/UpdateCaptionPresets.swift reference-only import Foundation import IMGLYEngine @MainActor func updateCaptionPresets(engine: Engine) async throws { // A caption block to apply presets to. Caption tracks and blocks are covered // in the Add Captions guide; this is the minimum a preset needs to target. let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.editor.setSettingBool("features/videoCaptionsEnabled", value: true) let captionTrack = try engine.block.create(.captionTrack) try engine.block.appendChild(to: page, child: captionTrack) let caption = try engine.block.create(.caption) try engine.block.setString(caption, property: "caption/text", value: "Your caption text") try engine.block.appendChild(to: captionTrack, child: caption) let neonGlowStyle = """ { "blockType": "//ly.img.ubq/caption", "mode": "replace", "typeface": { "family": "Manrope", "weight": "bold", "style": "normal" }, "properties": { "caption/horizontalAlignment": "Center", "caption/verticalAlignment": "Center", "fill/enabled": true, "fill/solid/color": { "r": 0, "g": 1, "b": 1, "a": 1 }, "dropShadow/enabled": true, "dropShadow/color": { "r": 0, "g": 1, "b": 1, "a": 1 }, "dropShadow/offset/x": 0, "dropShadow/offset/y": 0, "dropShadow/blurRadius/x": 8, "dropShadow/blurRadius/y": 8 }, "scaleWithFontSize": [ { "property": "dropShadow/blurRadius/x", "ratio": 0.2 }, { "property": "dropShadow/blurRadius/y", "ratio": 0.2 } ] } """ let contentJSON = """ { "version": "7.0.0", "id": "ly.img.caption.presets", "assets": [ { "id": "ly.img.caption.presets.neon-glow", "label": { "en": "Neon Glow" }, "groups": ["caption"], "meta": { "thumbUri": "{{base_url}}/ly.img.caption.presets/thumbnails/neon-glow.png" }, "payload": { "stylePreset": \(neonGlowStyle) } } ] } """ let contentURL = FileManager.default.temporaryDirectory .appendingPathComponent("ly.img.caption.presets-content.json") try contentJSON.write(to: contentURL, atomically: true, encoding: .utf8) let captionPresetsSourceID = try await engine.asset.addLocalAssetSourceFromJSON(contentURL) let boldBackgroundStyle = """ { "blockType": "//ly.img.ubq/caption", "mode": "replace", "properties": { "caption/horizontalAlignment": "Center", "fill/enabled": true, "fill/solid/color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "backgroundColor/enabled": true, "backgroundColor/color": { "r": 0, "g": 0, "b": 0, "a": 0.6 } } } """ let boldBackground = AssetDefinition( id: "ly.img.caption.presets.bold-background", groups: ["caption"], meta: ["thumbUri": "https://example.com/caption-presets/bold-background.png"], payload: AssetPayload(stylePreset: boldBackgroundStyle), label: ["en": "Bold Background"], ) try engine.asset.addAsset(to: captionPresetsSourceID, asset: boldBackground) let presets = try await engine.asset.findAssets( sourceID: captionPresetsSourceID, query: AssetQueryData(query: nil, page: 0, groups: ["caption"], perPage: 10), ) if let neonGlow = presets.assets.first(where: { $0.id == "ly.img.caption.presets.neon-glow" }) { try await engine.asset.applyToBlock(sourceID: captionPresetsSourceID, assetResult: neonGlow, block: caption) } } ``` Extend CE.SDK's video captions with custom caption presets. A caption preset is a declarative style preset — you describe the look as JSON and the engine applies it to a caption block. > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-update-caption-presets) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. A caption preset is a JSON document that the engine applies to a caption block — there are no blocks to build or files to serialize. CE.SDK groups these presets under the `ly.img.caption.presets` asset source. This guide defines a custom preset, hosts it in a `content.json`, registers another preset at runtime, and applies a preset to a caption with the engine's asset APIs. ## Define a Caption Style Preset Describe the look as a JSON document. In Swift a style preset is opaque — you author the JSON and the engine parses and applies it, so the document never needs a typed wrapper. ```swift highlight-updateCaptionPresets-defineStylePreset let neonGlowStyle = """ { "blockType": "//ly.img.ubq/caption", "mode": "replace", "typeface": { "family": "Manrope", "weight": "bold", "style": "normal" }, "properties": { "caption/horizontalAlignment": "Center", "caption/verticalAlignment": "Center", "fill/enabled": true, "fill/solid/color": { "r": 0, "g": 1, "b": 1, "a": 1 }, "dropShadow/enabled": true, "dropShadow/color": { "r": 0, "g": 1, "b": 1, "a": 1 }, "dropShadow/offset/x": 0, "dropShadow/offset/y": 0, "dropShadow/blurRadius/x": 8, "dropShadow/blurRadius/y": 8 }, "scaleWithFontSize": [ { "property": "dropShadow/blurRadius/x", "ratio": 0.2 }, { "property": "dropShadow/blurRadius/y", "ratio": 0.2 } ] } """ ``` The fields: - `blockType`: the target block. Use `"//ly.img.ubq/caption"` for caption presets. - `mode`: `"replace"` clears anything the preset omits, except the properties in `keepExisting`, so presets never stack; `"merge"` layers the preset on top and keeps untouched properties. - `typeface`: the font, resolved by `family` with optional `weight` and `style`. An unregistered family is skipped. - `properties`: a flat map of property paths to values. Keys without a slash are namespaced to the caption block (for example `caption/horizontalAlignment`); keys with a slash are used verbatim (`fill/solid/color`). Colors are RGBA objects with `r`, `g`, `b`, and an optional `a`, each in the 0–1 range. - `scaleWithFontSize`: keeps a decoration proportional to the font size. Each `{ property, ratio }` entry sets `property` to `ratio × fontSize`. Any numeric caption property works — common ones are `stroke/width`, the `dropShadow` offset and blur radius, and `backgroundColor/cornerRadius` and padding. - `keepExisting`: the properties that applying the preset leaves as the caption has them, for example `["text", "typeface"]` to keep the transcript and the user's font. The editor applies the preset to captions that already exist, also when it styles new captions, so a listed property never comes from the preset there. Without the list, a preset keeps the caption's text. A list replaces that default, so add `text` to keep the text. Animations follow `mode`, not this list. ## Host Presets in a content.json To serve presets from your own files, host a `content.json` for the `ly.img.caption.presets` source and load it with `addLocalAssetSourceFromJSON`. The source needs only the index file and a thumbnails folder — each asset carries its look inline in `payload.stylePreset`, so there are no separate preset files: ``` ly.img.caption.presets/ ├── content.json └── thumbnails/ └── neon-glow.png ``` ```swift highlight-updateCaptionPresets-hostPresets let contentJSON = """ { "version": "7.0.0", "id": "ly.img.caption.presets", "assets": [ { "id": "ly.img.caption.presets.neon-glow", "label": { "en": "Neon Glow" }, "groups": ["caption"], "meta": { "thumbUri": "{{base_url}}/ly.img.caption.presets/thumbnails/neon-glow.png" }, "payload": { "stylePreset": \(neonGlowStyle) } } ] } """ let contentURL = FileManager.default.temporaryDirectory .appendingPathComponent("ly.img.caption.presets-content.json") try contentJSON.write(to: contentURL, atomically: true, encoding: .utf8) let captionPresetsSourceID = try await engine.asset.addLocalAssetSourceFromJSON(contentURL) ``` The manifest uses `version` `"7.0.0"` and the `ly.img.caption.presets` source id. Each asset needs a unique `id`, a localized `label`, the `caption` group, a `meta.thumbUri` preview, and the declarative look in `payload.stylePreset`. Use the `{{base_url}}` placeholder for paths that resolve relative to the manifest's parent directory. `addLocalAssetSourceFromJSON(_:matcher:)` returns the source id. It registers the source if it does not exist yet, or merges the manifest's assets into it if it does — so a hosted manifest and runtime registration share the same `ly.img.caption.presets` source. ## Register a Preset at Runtime To add a preset without editing the `content.json`, build an `AssetDefinition` and add it to the source. The definition mirrors a hosted entry: an id, a localized label, the `caption` group, a thumbnail, and the declarative look in `payload.stylePreset`. `AssetPayload(stylePreset:)` carries the same opaque JSON document you author for the manifest. ```swift highlight-updateCaptionPresets-registerPreset let boldBackgroundStyle = """ { "blockType": "//ly.img.ubq/caption", "mode": "replace", "properties": { "caption/horizontalAlignment": "Center", "fill/enabled": true, "fill/solid/color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "backgroundColor/enabled": true, "backgroundColor/color": { "r": 0, "g": 0, "b": 0, "a": 0.6 } } } """ let boldBackground = AssetDefinition( id: "ly.img.caption.presets.bold-background", groups: ["caption"], meta: ["thumbUri": "https://example.com/caption-presets/bold-background.png"], payload: AssetPayload(stylePreset: boldBackgroundStyle), label: ["en": "Bold Background"], ) try engine.asset.addAsset(to: captionPresetsSourceID, asset: boldBackground) ``` `addAsset(to:asset:)` adds the preset to the existing `ly.img.caption.presets` source, alongside any hosted or built-in presets. ## Apply a Preset to a Caption Query the source and apply a result to a caption block. `applyToBlock(sourceID:assetResult:block:)` reads the asset's `payload.stylePreset` and applies it to the caption. `caption` here is a caption block on a caption track — see [Add Captions](https://img.ly/docs/cesdk/mac-catalyst/edit-video/add-captions-f67565/) for creating caption tracks and blocks. ```swift highlight-updateCaptionPresets-applyPreset let presets = try await engine.asset.findAssets( sourceID: captionPresetsSourceID, query: AssetQueryData(query: nil, page: 0, groups: ["caption"], perPage: 10), ) if let neonGlow = presets.assets.first(where: { $0.id == "ly.img.caption.presets.neon-glow" }) { try await engine.asset.applyToBlock(sourceID: captionPresetsSourceID, assetResult: neonGlow, block: caption) } ``` `findAssets(sourceID:query:)` returns the presets in the `caption` group regardless of insertion order, so select a specific preset by id rather than relying on the first result. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Preset not loading | The manifest is unreachable or `version` is wrong | Confirm the `content.json` URL resolves and `version` is `"7.0.0"` | | Styles not applying | `blockType` does not match the target | Set `blockType` to `"//ly.img.ubq/caption"` and apply to a caption block | | Color or decoration missing | RGBA out of range, or the decoration is disabled | Use values in the 0–1 range and enable a decoration before coloring it (for example `dropShadow/enabled` alongside `dropShadow/color`) | | Decoration not scaling | The property is not in `scaleWithFontSize` | Add it as a `{ property, ratio }` entry; any numeric caption property can scale (stroke width, drop-shadow offset and blur, background corner radius and padding) | ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addLocalAssetSourceFromJSON(_:matcher:)` | Load a hosted `content.json` manifest into an asset source | | `engine.asset.addAsset(to:asset:)` | Add a preset to an asset source at runtime | | `engine.asset.findAssets(sourceID:query:)` | Query the registered caption presets | | `engine.asset.applyToBlock(sourceID:assetResult:block:)` | Apply a preset to a caption block | ## Next Steps - [Add Captions](https://img.ly/docs/cesdk/mac-catalyst/edit-video/add-captions-f67565/) — Add captions to videos and understand caption tracks - [Remote Asset Sources](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/remote-asset-484685/) — Host and serve assets from custom locations - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) — Style text blocks with fonts, colors, and effects --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Edit Image" description: "Use CE.SDK to crop, transform, annotate, or enhance images with editing tools and programmatic APIs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) --- --- ## Related Pages - [Replace Colors](https://img.ly/docs/cesdk/mac-catalyst/edit-image/replace-colors-6ede17/) - Replace specific colors in images using CE.SDK's Recolor and Green Screen effects with programmatic control. - [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/) - Crop, resize, rotate, scale, or flip images using CE.SDK's built-in transformation tools. - [Annotation](https://img.ly/docs/cesdk/mac-catalyst/edit-image/annotation-142604/) - Add shape-based annotations to images and designs with CreativeEngine. - [Add Watermark](https://img.ly/docs/cesdk/mac-catalyst/edit-image/add-watermark-679de0/) - Add text and image watermarks to protect images, indicate ownership, or add branding with the CE.SDK Engine on Apple platforms. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Add Watermark" description: "Add text and image watermarks to protect images, indicate ownership, or add branding with the CE.SDK Engine on Apple platforms." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-image/add-watermark-679de0/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) > [Add Watermark](https://img.ly/docs/cesdk/mac-catalyst/edit-image/add-watermark-679de0/) --- ```swift file=@cesdk_swift_examples/engine-guides-edit-image-add-watermark/AddImageWatermark.swift reference-only import Foundation import IMGLYEngine @MainActor func addImageWatermark(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") try await engine.scene.create(fromImage: imageURL) guard let page = try engine.scene.getCurrentPage() else { fatalError("Expected create(fromImage:) to create a page.") } let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) let textWatermark = try engine.block.create(.text) try engine.block.replaceText(textWatermark, text: "All rights reserved") try engine.block.setHeightMode(textWatermark, mode: .auto) try engine.block.setWidth(textWatermark, value: pageWidth * 0.55) try engine.block.appendChild(to: page, child: textWatermark) try engine.block.setTextFontSize(textWatermark, fontSize: 28) try engine.block.setTextColor(textWatermark, color: .rgba(r: 1, g: 1, b: 1, a: 1)) try engine.block.setTextHorizontalAlignment(textWatermark, alignment: .left) try engine.block.setOpacity(textWatermark, value: 0.7) try await engine.captureGuide(page, label: "after-text") let logoWatermark = try engine.block.create(.graphic) try engine.block.setShape(logoWatermark, shape: engine.block.createShape(.rect)) let logoFill = try engine.block.createFill(.image) let logoURL = baseURL.appendingPathComponent( "ly.img.sticker/images/3Dstickers/3d_stickers_megaphone.png", ) try engine.block.setURL(logoFill, property: "fill/image/imageFileURI", value: logoURL) try engine.block.setFill(logoWatermark, fill: logoFill) try engine.block.setContentFillMode(logoWatermark, mode: .contain) try engine.block.appendChild(to: page, child: logoWatermark) let logoSize = pageWidth * 0.14 try engine.block.setWidth(logoWatermark, value: logoSize) try engine.block.setHeight(logoWatermark, value: logoSize) try engine.block.setOpacity(logoWatermark, value: 0.62) let spacing: Float = 18 let bottomPadding: Float = 36 let textWidth = try engine.block.getFrameWidth(textWatermark) let textHeight = try engine.block.getFrameHeight(textWatermark) let totalWatermarkWidth = logoSize + spacing + textWidth let startX = (pageWidth - totalWatermarkWidth) / 2 let centerY = pageHeight - bottomPadding - max(logoSize, textHeight) / 2 let logoY = centerY - logoSize / 2 let textY = centerY - textHeight / 2 try engine.block.setPositionX(logoWatermark, value: startX) try engine.block.setPositionY(logoWatermark, value: logoY) try engine.block.setPositionX(textWatermark, value: startX + logoSize + spacing) try engine.block.setPositionY(textWatermark, value: textY) try await engine.captureGuide(page, label: "after-position") for watermark in [textWatermark, logoWatermark] { guard try engine.block.supportsDropShadow(watermark) else { continue } try engine.block.setDropShadowEnabled(watermark, enabled: true) try engine.block.setDropShadowColor(watermark, color: .rgba(r: 0, g: 0, b: 0, a: 0.55)) try engine.block.setDropShadowOffsetX(watermark, offsetX: 3) try engine.block.setDropShadowOffsetY(watermark, offsetY: 3) try engine.block.setDropShadowBlurRadiusX(watermark, blurRadiusX: 6) try engine.block.setDropShadowBlurRadiusY(watermark, blurRadiusY: 6) } try await engine.captureGuide(page, label: "hero") let exportedPNG = try await engine.block.export(page, mimeType: .png) let exportedJPEG = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.86), ) let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) try exportedPNG.write(to: tempDir.appendingPathComponent("watermarked.png")) try exportedJPEG.write(to: tempDir.appendingPathComponent("watermarked.jpg")) } ``` Add text and image watermarks to images programmatically using CE.SDK's Engine block API on Apple platforms. ![An image with a logo and copyright text watermark anchored to the bottom of the page, both softened by a drop shadow.](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-edit-image-add-watermark) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Watermarks protect intellectual property, indicate ownership, add branding, or mark content as drafts. CE.SDK supports two types of watermarks: **text watermarks** created from text blocks for copyright notices and brand names, and **image watermarks** created from graphic blocks with image fills for logos and symbols. This guide covers how to create text and logo watermarks, position them on a design, style them for visibility, and export the watermarked result. ## Setup and Prerequisites Start from an image scene and read the page dimensions. The page provides the canvas where we add and position watermarks. ```swift highlight-addImageWatermark-setup let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") try await engine.scene.create(fromImage: imageURL) guard let page = try engine.scene.getCurrentPage() else { fatalError("Expected create(fromImage:) to create a page.") } let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) ``` `engine.scene.create(fromImage:)` creates a scene with a single page that displays the source image. `engine.scene.getCurrentPage()` returns the page block, and `getWidth(_:)` and `getHeight(_:)` provide the values used for placement calculations later. ## Creating Text Watermarks Text watermarks display copyright notices, URLs, or brand names. We create a text block, set its content, and add it to the page. ```swift highlight-addImageWatermark-createTextWatermark let textWatermark = try engine.block.create(.text) try engine.block.replaceText(textWatermark, text: "All rights reserved") try engine.block.setHeightMode(textWatermark, mode: .auto) try engine.block.setWidth(textWatermark, value: pageWidth * 0.55) try engine.block.appendChild(to: page, child: textWatermark) ``` `engine.block.create(.text)` creates a text block. `setWidth(_:value:)` defines the text wrap boundary as a fraction of the page width, and `SizeMode.auto` on the height lets the block grow vertically to fit the content if it ever wraps to multiple lines. ### Styling the Text Configure the font size, color, alignment, and opacity to make the watermark visible without dominating the image. ```swift highlight-addImageWatermark-styleTextWatermark try engine.block.setTextFontSize(textWatermark, fontSize: 28) try engine.block.setTextColor(textWatermark, color: .rgba(r: 1, g: 1, b: 1, a: 1)) try engine.block.setTextHorizontalAlignment(textWatermark, alignment: .left) try engine.block.setOpacity(textWatermark, value: 0.7) ``` Key styling options: - **Font size** — Use a size that remains readable at the target export dimensions. - **Text color** — White or black usually works best, depending on the image background. - **Opacity** — Values between `0.5` and `0.7` provide a balanced semi-transparent appearance. ## Creating Logo Watermarks Logo watermarks use graphic blocks with image fills to display brand symbols or company logos. ```swift highlight-addImageWatermark-createLogoWatermark let logoWatermark = try engine.block.create(.graphic) try engine.block.setShape(logoWatermark, shape: engine.block.createShape(.rect)) let logoFill = try engine.block.createFill(.image) let logoURL = baseURL.appendingPathComponent( "ly.img.sticker/images/3Dstickers/3d_stickers_megaphone.png", ) try engine.block.setURL(logoFill, property: "fill/image/imageFileURI", value: logoURL) try engine.block.setFill(logoWatermark, fill: logoFill) try engine.block.setContentFillMode(logoWatermark, mode: .contain) try engine.block.appendChild(to: page, child: logoWatermark) ``` We create a graphic block, assign a rect shape, then create an image fill with the logo URL. `ContentFillMode.contain` keeps the logo inside its frame without cropping, regardless of the source image aspect ratio. ### Sizing the Logo Set dimensions for the logo and apply opacity to match the text watermark. ```swift highlight-addImageWatermark-sizeLogoWatermark let logoSize = pageWidth * 0.14 try engine.block.setWidth(logoWatermark, value: logoSize) try engine.block.setHeight(logoWatermark, value: logoSize) try engine.block.setOpacity(logoWatermark, value: 0.62) ``` A useful rule is to size logos to 10–20% of the page width. This keeps them visible without covering the main image content. ### Positioning the Watermarks Calculate positions from the current page dimensions. This sample places the logo and text side by side near the bottom center. ```swift highlight-addImageWatermark-positionWatermarks let spacing: Float = 18 let bottomPadding: Float = 36 let textWidth = try engine.block.getFrameWidth(textWatermark) let textHeight = try engine.block.getFrameHeight(textWatermark) let totalWatermarkWidth = logoSize + spacing + textWidth let startX = (pageWidth - totalWatermarkWidth) / 2 let centerY = pageHeight - bottomPadding - max(logoSize, textHeight) / 2 let logoY = centerY - logoSize / 2 let textY = centerY - textHeight / 2 try engine.block.setPositionX(logoWatermark, value: startX) try engine.block.setPositionY(logoWatermark, value: logoY) try engine.block.setPositionX(textWatermark, value: startX + logoSize + spacing) try engine.block.setPositionY(textWatermark, value: textY) ``` The sample reads the text block's rendered dimensions with `getFrameWidth` and `getFrameHeight` so the layout stays accurate when the text content, wrap width, or font size changes. It combines the logo size, the text width, and a fixed spacing to compute the total watermark width, then centers the group horizontally with `startX`. Vertically, it derives a shared `centerY` from the taller of the two blocks so both elements sit on a common baseline while respecting `bottomPadding`. ## Enhancing Visibility with Drop Shadows Drop shadows improve watermark readability against varied backgrounds by adding contrast. ```swift highlight-addImageWatermark-addDropShadow for watermark in [textWatermark, logoWatermark] { guard try engine.block.supportsDropShadow(watermark) else { continue } try engine.block.setDropShadowEnabled(watermark, enabled: true) try engine.block.setDropShadowColor(watermark, color: .rgba(r: 0, g: 0, b: 0, a: 0.55)) try engine.block.setDropShadowOffsetX(watermark, offsetX: 3) try engine.block.setDropShadowOffsetY(watermark, offsetY: 3) try engine.block.setDropShadowBlurRadiusX(watermark, blurRadiusX: 6) try engine.block.setDropShadowBlurRadiusY(watermark, blurRadiusY: 6) } ``` `supportsDropShadow(_:)` guards the call so the loop works for any future block type that does not carry a drop shadow component. The shadow parameters that matter: - **Offset X/Y** — Distance from the block; `2`–`4` works well for subtle watermarks. - **Blur Radius X/Y** — Softness of the shadow; `4`–`8` adds contrast without harsh edges. - **Color** — Black with partial alpha provides contrast without overpowering the image. ## Exporting Watermarked Images After adding watermarks, export the complete page as an image file. ```swift highlight-addImageWatermark-exportWatermarked let exportedPNG = try await engine.block.export(page, mimeType: .png) let exportedJPEG = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.86), ) let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) try exportedPNG.write(to: tempDir.appendingPathComponent("watermarked.png")) try exportedJPEG.write(to: tempDir.appendingPathComponent("watermarked.jpg")) ``` `engine.block.export(_:mimeType:options:)` renders the page with all watermarks and returns the encoded image data. Pass a different `MIMEType` to switch formats — `.png` for lossless output, `.jpeg` for smaller files with the `jpegQuality` option, or `.webp` when both size and quality matter. ## Troubleshooting **Watermark not visible** - Verify the block is within page bounds using its position values. - Check that opacity is between `0.3` and `1.0`. - Ensure `appendChild(to:child:)` was called to add the block to the page. **Position appears incorrect** - Recalculate positions using the current page width and height. - Account for watermark dimensions when computing corner or centered positions. - Remember that coordinates start from the top-left corner. **Text not legible** - Increase the font size relative to the target export dimensions. - Add a drop shadow for contrast against complex backgrounds. - Increase opacity if the watermark is too faint. **Logo quality issues** - Use a higher-resolution source image for the logo. - Avoid scaling the logo beyond its original dimensions. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.create(fromImage:)` | Create a scene with a single page from an image URL | | `engine.scene.getCurrentPage()` | Get the page created for the image scene | | `engine.block.getWidth(_:)` | Read the page width for placement calculations | | `engine.block.getHeight(_:)` | Read the page height for placement calculations | | `engine.block.create(_:)` | Create a text or graphic block | | `engine.block.setHeightMode(_:mode:)` | Let the text block auto-grow vertically with its content | | `engine.block.replaceText(_:text:)` | Set the text watermark content | | `engine.block.appendChild(to:child:)` | Add the watermark to the page | | `engine.block.setTextFontSize(_:fontSize:)` | Set the text size | | `engine.block.setTextColor(_:color:)` | Set the text color | | `engine.block.setTextHorizontalAlignment(_:alignment:)` | Set paragraph alignment | | `engine.block.setOpacity(_:value:)` | Set watermark transparency | | `engine.block.createShape(_:)` | Create a shape for a graphic block | | `engine.block.setShape(_:shape:)` | Apply the shape to the graphic block | | `engine.block.createFill(_:)` | Create a fill for a graphic block | | `engine.block.setURL(_:property:value:)` | Set the logo image URL on the fill | | `engine.block.setFill(_:fill:)` | Apply the image fill to the graphic block | | `engine.block.setContentFillMode(_:mode:)` | Fit the logo inside its frame | | `engine.block.setWidth(_:value:)` | Set the watermark width | | `engine.block.setHeight(_:value:)` | Set the watermark height | | `engine.block.setPositionX(_:value:)` | Set the horizontal position | | `engine.block.setPositionY(_:value:)` | Set the vertical position | | `engine.block.supportsDropShadow(_:)` | Check whether a block supports drop shadows | | `engine.block.setDropShadowEnabled(_:enabled:)` | Enable or disable a drop shadow | | `engine.block.setDropShadowColor(_:color:)` | Set the shadow color and alpha | | `engine.block.setDropShadowOffsetX(_:offsetX:)` | Set the horizontal shadow offset | | `engine.block.setDropShadowOffsetY(_:offsetY:)` | Set the vertical shadow offset | | `engine.block.setDropShadowBlurRadiusX(_:blurRadiusX:)` | Set the horizontal shadow blur | | `engine.block.setDropShadowBlurRadiusY(_:blurRadiusY:)` | Set the vertical shadow blur | | `engine.block.export(_:mimeType:options:)` | Export the watermarked page | ### Enums | Enum | Description | | --- | --- | | `DesignBlockType.text` / `.graphic` | Block type used for text and image watermarks | | `ShapeType.rect` | Rectangular shape for the logo block | | `FillType.image` | Image fill for the logo block | | `SizeMode.auto` | Height mode that lets the text block grow vertically with its content | | `ContentFillMode.contain` | Keep the logo inside its frame without cropping | | `HorizontalTextAlignment.left` | Paragraph alignment for the text watermark | | `MIMEType.png` / `.jpeg` / `.webp` | Output formats for the exported image | ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) — Style text blocks with fonts, colors, and effects - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Export options and formats for watermarked images - [Crop Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/crop-f67a47/) — Transform images before watermarking --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Annotation" description: "Add shape-based annotations to images and designs with CreativeEngine." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-image/annotation-142604/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) > [Annotation](https://img.ly/docs/cesdk/mac-catalyst/edit-image/annotation-142604/) --- ```swift file=@cesdk_swift_examples/engine-guides-annotation/ImageAnnotation.swift reference-only import IMGLYEngine @MainActor func imageAnnotation(engine: Engine) async throws { // Demo scaffolding: a page with a light placeholder rectangle that stands in // for an image so the annotations rendered below have something to sit on // top of in the captured hero. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let imageArea = try engine.block.create(.graphic) try engine.block.setShape(imageArea, shape: engine.block.createShape(.rect)) let imageAreaFill = try engine.block.createFill(.color) try engine.block.setColor( imageAreaFill, property: "fill/color/value", color: .rgba(r: 0.92, g: 0.94, b: 0.96, a: 1.0), ) try engine.block.setFill(imageArea, fill: imageAreaFill) try engine.block.setPositionX(imageArea, value: 40) try engine.block.setPositionY(imageArea, value: 40) try engine.block.setWidth(imageArea, value: 720) try engine.block.setHeight(imageArea, value: 520) try engine.block.appendChild(to: page, child: imageArea) let highlight = try addRectangleAnnotation(engine: engine, page: page) _ = try addCircleAnnotation(engine: engine, page: page) _ = try addLineAnnotation(engine: engine, page: page) _ = try addRedactionBox(engine: engine, page: page) try styleRectangleAnnotationAppearance(engine: engine, rectangle: highlight) try await engine.captureGuide(page, label: "hero") } @MainActor func addRectangleAnnotation(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let highlight = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(highlight, shape: rectShape) try engine.block.setPositionX(highlight, value: 100) try engine.block.setPositionY(highlight, value: 100) try engine.block.setWidth(highlight, value: 220) try engine.block.setHeight(highlight, value: 90) let fill = try engine.block.createFill(.color) try engine.block.setColor( fill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.82, b: 0.0, a: 0.4), ) try engine.block.setFill(highlight, fill: fill) try engine.block.appendChild(to: page, child: highlight) return highlight } @MainActor func addCircleAnnotation(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let callout = try engine.block.create(.graphic) let ellipseShape = try engine.block.createShape(.ellipse) try engine.block.setShape(callout, shape: ellipseShape) try engine.block.setPositionX(callout, value: 360) try engine.block.setPositionY(callout, value: 155) try engine.block.setWidth(callout, value: 120) try engine.block.setHeight(callout, value: 120) try engine.block.setFillEnabled(callout, enabled: false) try engine.block.setStrokeEnabled(callout, enabled: true) try engine.block.setStrokeColor(callout, color: .rgba(r: 1.0, g: 0.0, b: 0.0, a: 1.0)) try engine.block.setStrokeWidth(callout, width: 4) try engine.block.appendChild(to: page, child: callout) return callout } @MainActor func addLineAnnotation(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let underline = try engine.block.create(.graphic) let lineShape = try engine.block.createShape(.line) try engine.block.setShape(underline, shape: lineShape) try engine.block.setPositionX(underline, value: 85) try engine.block.setPositionY(underline, value: 430) try engine.block.setWidth(underline, value: 320) let lineThickness: Float = 8 try engine.block.setHeight(underline, value: lineThickness) try engine.block.setStrokeEnabled(underline, enabled: true) try engine.block.setStrokeColor(underline, color: .rgba(r: 0.05, g: 0.25, b: 0.95, a: 1.0)) try engine.block.setStrokeWidth(underline, width: lineThickness) try engine.block.appendChild(to: page, child: underline) return underline } @MainActor func addRedactionBox(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let redaction = try engine.block.create(.graphic) try engine.block.setShape(redaction, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(redaction, value: 500) try engine.block.setPositionY(redaction, value: 360) try engine.block.setWidth(redaction, value: 180) try engine.block.setHeight(redaction, value: 34) let fill = try engine.block.createFill(.color) try engine.block.setColor( fill, property: "fill/color/value", color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 1.0), ) try engine.block.setFill(redaction, fill: fill) try engine.block.appendChild(to: page, child: redaction) return redaction } @MainActor func styleRectangleAnnotationAppearance(engine: Engine, rectangle: DesignBlockID) throws { try engine.block.setOpacity(rectangle, value: 0.5) let shape = try engine.block.getShape(rectangle) try engine.block.setFloat(shape, property: "shape/rect/cornerRadiusTL", value: 10) try engine.block.setFloat(shape, property: "shape/rect/cornerRadiusTR", value: 10) try engine.block.setFloat(shape, property: "shape/rect/cornerRadiusBL", value: 10) try engine.block.setFloat(shape, property: "shape/rect/cornerRadiusBR", value: 10) try engine.block.setStrokeEnabled(rectangle, enabled: true) try engine.block.setStrokeStyle(rectangle, style: .dashed) try engine.block.setStrokeWidth(rectangle, width: 3) try engine.block.setStrokeColor(rectangle, color: .rgba(r: 0.9, g: 0.35, b: 0.0, a: 1.0)) } ``` Add rectangles, circles, lines, and redaction boxes on top of images or designs with shape blocks. ![Shape annotations — a translucent yellow rectangle with rounded corners and a dashed orange outline highlights an area, a red ellipse outlines a callout, a thick blue line underlines a region, and a solid black bar covers redacted text](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-annotation) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Annotations in CE.SDK are graphic blocks with shape geometry. Use them to highlight important areas, circle details, underline content, hide sensitive information, or add visual review marks on top of existing page content. The examples below append annotations to an existing page. Position and dimensions are set on the graphic block, while rectangle-specific properties such as corner radius are set on the attached shape block. ## Add Rectangle Annotation Create a graphic block, attach a rectangle shape, position it, and give it a semi-transparent color fill. A transparent fill keeps the underlying image or design visible while drawing attention to a region. ```swift highlight-imageAnnotation-rectangle @MainActor func addRectangleAnnotation(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let highlight = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(highlight, shape: rectShape) try engine.block.setPositionX(highlight, value: 100) try engine.block.setPositionY(highlight, value: 100) try engine.block.setWidth(highlight, value: 220) try engine.block.setHeight(highlight, value: 90) let fill = try engine.block.createFill(.color) try engine.block.setColor( fill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.82, b: 0.0, a: 0.4), ) try engine.block.setFill(highlight, fill: fill) try engine.block.appendChild(to: page, child: highlight) return highlight } ``` ## Add Circle Annotation Use an ellipse shape for circles and callouts. Disable the fill and enable a stroke when the annotation should outline an item without covering it. ```swift highlight-imageAnnotation-circle @MainActor func addCircleAnnotation(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let callout = try engine.block.create(.graphic) let ellipseShape = try engine.block.createShape(.ellipse) try engine.block.setShape(callout, shape: ellipseShape) try engine.block.setPositionX(callout, value: 360) try engine.block.setPositionY(callout, value: 155) try engine.block.setWidth(callout, value: 120) try engine.block.setHeight(callout, value: 120) try engine.block.setFillEnabled(callout, enabled: false) try engine.block.setStrokeEnabled(callout, enabled: true) try engine.block.setStrokeColor(callout, color: .rgba(r: 1.0, g: 0.0, b: 0.0, a: 1.0)) try engine.block.setStrokeWidth(callout, width: 4) try engine.block.appendChild(to: page, child: callout) return callout } ``` Use equal width and height for a circle. Different values produce an ellipse. ## Add Line Annotation Use a line shape for underlines, separators, and markup strokes. The block width controls the line length. ```swift highlight-imageAnnotation-line @MainActor func addLineAnnotation(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let underline = try engine.block.create(.graphic) let lineShape = try engine.block.createShape(.line) try engine.block.setShape(underline, shape: lineShape) try engine.block.setPositionX(underline, value: 85) try engine.block.setPositionY(underline, value: 430) try engine.block.setWidth(underline, value: 320) let lineThickness: Float = 8 try engine.block.setHeight(underline, value: lineThickness) try engine.block.setStrokeEnabled(underline, enabled: true) try engine.block.setStrokeColor(underline, color: .rgba(r: 0.05, g: 0.25, b: 0.95, a: 1.0)) try engine.block.setStrokeWidth(underline, width: lineThickness) try engine.block.appendChild(to: page, child: underline) return underline } ``` Line shapes use stroke width for the visible thickness. Keep the block height in sync with the stroke width so the line bounds match the rendered stroke. ## Add a Visual Redaction Overlay To visually cover content in flattened image output, place a solid rectangle over the sensitive area. Use an opaque fill so the covered content is not visible in the final flattened export. The original block remains underneath the overlay in the editable CE.SDK scene and in layered or reusable outputs. For sensitive content, remove, crop, or replace the source content before sharing an editable scene. ```swift highlight-imageAnnotation-redaction @MainActor func addRedactionBox(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let redaction = try engine.block.create(.graphic) try engine.block.setShape(redaction, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(redaction, value: 500) try engine.block.setPositionY(redaction, value: 360) try engine.block.setWidth(redaction, value: 180) try engine.block.setHeight(redaction, value: 34) let fill = try engine.block.createFill(.color) try engine.block.setColor( fill, property: "fill/color/value", color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 1.0), ) try engine.block.setFill(redaction, fill: fill) try engine.block.appendChild(to: page, child: redaction) return redaction } ``` ## Style Annotation Appearance Common rectangle styling changes include opacity, rounded corners, and dashed strokes. Apply opacity and stroke settings to the graphic block, then update rectangle-specific corner geometry on the attached shape block. ```swift highlight-imageAnnotation-style @MainActor func styleRectangleAnnotationAppearance(engine: Engine, rectangle: DesignBlockID) throws { try engine.block.setOpacity(rectangle, value: 0.5) let shape = try engine.block.getShape(rectangle) try engine.block.setFloat(shape, property: "shape/rect/cornerRadiusTL", value: 10) try engine.block.setFloat(shape, property: "shape/rect/cornerRadiusTR", value: 10) try engine.block.setFloat(shape, property: "shape/rect/cornerRadiusBL", value: 10) try engine.block.setFloat(shape, property: "shape/rect/cornerRadiusBR", value: 10) try engine.block.setStrokeEnabled(rectangle, enabled: true) try engine.block.setStrokeStyle(rectangle, style: .dashed) try engine.block.setStrokeWidth(rectangle, width: 3) try engine.block.setStrokeColor(rectangle, color: .rgba(r: 0.9, g: 0.35, b: 0.0, a: 1.0)) } ``` ## Troubleshooting | Issue | Solution | |-------|----------| | Shape not visible | Check that the block is appended to the page and that fill or stroke is enabled. | | Shape positioned incorrectly | Verify x and y positions on the graphic block, not the shape block. | | Stroke not showing | Enable stroke and set a stroke width greater than `0`. | | Rounded corners not changing | Set the corner radius properties on the rectangle shape returned by `getShape(_:)`. | | Shape covers too much content | Lower block opacity or use a stroke-only annotation. | ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.create(_:)` | Create a design block such as a graphic block (`.graphic`). | | `engine.block.createShape(_:)` | Create a shape block such as `.rect`, `.ellipse`, or `.line`. | | `engine.block.setShape(_:shape:)` | Attach a shape block to a graphic block. | | `engine.block.setPositionX(_:value:)` | Set the graphic block's x position. | | `engine.block.setPositionY(_:value:)` | Set the graphic block's y position. | | `engine.block.setWidth(_:value:)` | Set the graphic block's width. | | `engine.block.setHeight(_:value:)` | Set the graphic block's height. | | `engine.block.createFill(_:)` | Create a fill block such as `.color`. | | `engine.block.setFill(_:fill:)` | Assign a fill to a graphic block. | | `engine.block.setFillEnabled(_:enabled:)` | Enable or disable the graphic block's fill. | | `engine.block.setStrokeEnabled(_:enabled:)` | Enable or disable the graphic block's stroke. | | `engine.block.setStrokeColor(_:color:)` | Set the graphic block's stroke color. | | `engine.block.setStrokeWidth(_:width:)` | Set the graphic block's stroke width. | | `engine.block.setStrokeStyle(_:style:)` | Set a stroke style such as `.dashed`. | | `engine.block.setOpacity(_:value:)` | Set block opacity from `0` to `1`. | | `engine.block.getShape(_:)` | Get the shape block attached to a graphic block. | | `engine.block.setColor(_:property:color:)` | Set a typed color property. | | `engine.block.setFloat(_:property:value:)` | Set float shape properties such as rectangle corner radius. | | `engine.block.appendChild(to:child:)` | Add the annotation block to a page or container. | ### Properties | Property | Type | Description | | --- | --- | --- | | `fill/color/value` | Color | Solid color of a color fill. | | `shape/rect/cornerRadiusTL` | Float | Top-left corner radius of a rectangle shape. | | `shape/rect/cornerRadiusTR` | Float | Top-right corner radius of a rectangle shape. | | `shape/rect/cornerRadiusBL` | Float | Bottom-left corner radius of a rectangle shape. | | `shape/rect/cornerRadiusBR` | Float | Bottom-right corner radius of a rectangle shape. | ## Next Steps - [Transform Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/) — Crop, resize, rotate, scale, or flip image content. - [Edit Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) — Modify shape geometry, color, size, position, and corner radius. - [Grouping](https://img.ly/docs/cesdk/mac-catalyst/create-composition/group-and-ungroup-62565a/) — Group multiple annotations together. - [Layer Management](https://img.ly/docs/cesdk/mac-catalyst/create-composition/layer-management-18f07a/) — Control annotation stacking order. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Replace Colors" description: "Replace specific colors in images using CE.SDK's Recolor and Green Screen effects with programmatic control." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-image/replace-colors-6ede17/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) > [Replace Colors](https://img.ly/docs/cesdk/mac-catalyst/edit-image/replace-colors-6ede17/) --- ```swift file=@cesdk_swift_examples/engine-guides-colors-replace/ColorsReplace.swift reference-only import Foundation import IMGLYEngine @MainActor func colorsReplace(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 450) try engine.block.appendChild(to: scene, child: page) let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") // Create a Recolor effect that swaps red pixels for blue, then attach it to // an image block using `appendEffect`. let recolorBlock = try engine.block.create(.graphic) try engine.block.setShape(recolorBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(recolorBlock, value: 50) try engine.block.setPositionY(recolorBlock, value: 50) try engine.block.setWidth(recolorBlock, value: 200) try engine.block.setHeight(recolorBlock, value: 150) try engine.block.appendChild(to: page, child: recolorBlock) let recolorFill = try engine.block.createFill(.image) try engine.block.setURL(recolorFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(recolorBlock, fill: recolorFill) let recolorEffect = try engine.block.createEffect(.recolor) try engine.block.setColor( recolorEffect, property: "effect/recolor/fromColor", color: .rgba(r: 1, g: 0, b: 0, a: 1), ) try engine.block.setColor( recolorEffect, property: "effect/recolor/toColor", color: .rgba(r: 0, g: 0.5, b: 1, a: 1), ) try engine.block.appendEffect(recolorBlock, effectID: recolorEffect) try await engine.captureGuide(page, label: "after-recolor") let tolerancesBlock = try engine.block.create(.graphic) try engine.block.setShape(tolerancesBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(tolerancesBlock, value: 300) try engine.block.setPositionY(tolerancesBlock, value: 50) try engine.block.setWidth(tolerancesBlock, value: 200) try engine.block.setHeight(tolerancesBlock, value: 150) try engine.block.appendChild(to: page, child: tolerancesBlock) let tolerancesFill = try engine.block.createFill(.image) try engine.block.setURL(tolerancesFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(tolerancesBlock, fill: tolerancesFill) let tolerancesEffect = try engine.block.createEffect(.recolor) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/colorMatch", value: 0.3) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/brightnessMatch", value: 0.2) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/smoothness", value: 0.1) try engine.block.setColor( tolerancesEffect, property: "effect/recolor/fromColor", color: .rgba(r: 0.8, g: 0.6, b: 0.4, a: 1), ) try engine.block.setColor( tolerancesEffect, property: "effect/recolor/toColor", color: .rgba(r: 0.3, g: 0.7, b: 0.3, a: 1), ) try engine.block.appendEffect(tolerancesBlock, effectID: tolerancesEffect) // Create a Green Screen effect. `fromColor` picks the color to remove; any // pixel close enough to that color becomes transparent. let greenScreenBlock = try engine.block.create(.graphic) try engine.block.setShape(greenScreenBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(greenScreenBlock, value: 550) try engine.block.setPositionY(greenScreenBlock, value: 50) try engine.block.setWidth(greenScreenBlock, value: 200) try engine.block.setHeight(greenScreenBlock, value: 150) try engine.block.appendChild(to: page, child: greenScreenBlock) let greenScreenFill = try engine.block.createFill(.image) try engine.block.setURL(greenScreenFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(greenScreenBlock, fill: greenScreenFill) let greenScreenEffect = try engine.block.createEffect(.greenScreen) try engine.block.setColor( greenScreenEffect, property: "effect/green_screen/fromColor", color: .rgba(r: 0, g: 1, b: 0, a: 1), ) try engine.block.appendEffect(greenScreenBlock, effectID: greenScreenEffect) try await engine.captureGuide(page, label: "after-green-screen") let spillBlock = try engine.block.create(.graphic) try engine.block.setShape(spillBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(spillBlock, value: 50) try engine.block.setPositionY(spillBlock, value: 250) try engine.block.setWidth(spillBlock, value: 200) try engine.block.setHeight(spillBlock, value: 150) try engine.block.appendChild(to: page, child: spillBlock) let spillFill = try engine.block.createFill(.image) try engine.block.setURL(spillFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(spillBlock, fill: spillFill) let spillEffect = try engine.block.createEffect(.greenScreen) try engine.block.setFloat(spillEffect, property: "effect/green_screen/colorMatch", value: 0.4) try engine.block.setFloat(spillEffect, property: "effect/green_screen/smoothness", value: 0.2) try engine.block.setFloat(spillEffect, property: "effect/green_screen/spill", value: 0.5) try engine.block.setColor( spillEffect, property: "effect/green_screen/fromColor", color: .rgba(r: 0.2, g: 0.8, b: 0.3, a: 1), ) try engine.block.appendEffect(spillBlock, effectID: spillEffect) // Stack multiple Recolor effects on a single block, then toggle individual // entries with `setEffectEnabled` without removing them from the stack. let stackedBlock = try engine.block.create(.graphic) try engine.block.setShape(stackedBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(stackedBlock, value: 300) try engine.block.setPositionY(stackedBlock, value: 250) try engine.block.setWidth(stackedBlock, value: 200) try engine.block.setHeight(stackedBlock, value: 150) try engine.block.appendChild(to: page, child: stackedBlock) let stackedFill = try engine.block.createFill(.image) try engine.block.setURL(stackedFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(stackedBlock, fill: stackedFill) let redToBlue = try engine.block.createEffect(.recolor) try engine.block.setColor(redToBlue, property: "effect/recolor/fromColor", color: .rgba(r: 1, g: 0, b: 0, a: 1)) try engine.block.setColor(redToBlue, property: "effect/recolor/toColor", color: .rgba(r: 0, g: 0, b: 1, a: 1)) try engine.block.appendEffect(stackedBlock, effectID: redToBlue) let greenToOrange = try engine.block.createEffect(.recolor) try engine.block.setColor(greenToOrange, property: "effect/recolor/fromColor", color: .rgba(r: 0, g: 1, b: 0, a: 1)) try engine.block.setColor(greenToOrange, property: "effect/recolor/toColor", color: .rgba(r: 1, g: 0.5, b: 0, a: 1)) try engine.block.appendEffect(stackedBlock, effectID: greenToOrange) let stackedEffects = try engine.block.getEffects(stackedBlock) print("Number of effects: \(stackedEffects.count)") // 2 try engine.block.setEffectEnabled(effectID: stackedEffects[0], enabled: false) let isEnabled = try engine.block.isEffectEnabled(effectID: stackedEffects[0]) print("First effect enabled: \(isEnabled)") // false // Apply a consistent Recolor effect to every graphic block in the scene. // Skip blocks that already carry an effect so existing work isn't overwritten. let batchBlock = try engine.block.create(.graphic) try engine.block.setShape(batchBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(batchBlock, value: 550) try engine.block.setPositionY(batchBlock, value: 250) try engine.block.setWidth(batchBlock, value: 200) try engine.block.setHeight(batchBlock, value: 150) try engine.block.appendChild(to: page, child: batchBlock) let batchFill = try engine.block.createFill(.image) try engine.block.setURL(batchFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(batchBlock, fill: batchFill) let allGraphicBlocks = try engine.block.find(byType: .graphic) for blockID in allGraphicBlocks { if try engine.block.getEffects(blockID).isEmpty == false { continue } let batchRecolor = try engine.block.createEffect(.recolor) try engine.block.setColor( batchRecolor, property: "effect/recolor/fromColor", color: .rgba(r: 0.8, g: 0.7, b: 0.6, a: 1), ) try engine.block.setColor( batchRecolor, property: "effect/recolor/toColor", color: .rgba(r: 0.6, g: 0.7, b: 0.9, a: 1), ) try engine.block.setFloat(batchRecolor, property: "effect/recolor/colorMatch", value: 0.25) try engine.block.appendEffect(blockID, effectID: batchRecolor) } try await engine.captureGuide(page, label: "hero") } ``` Swap one color for another with the Recolor effect, or remove backgrounds with the Green Screen effect, both attached to graphic blocks via the effect stack. ![A 3x2 grid of the same source photo: the top row shows a basic red-to-blue Recolor, a tuned tan-to-green Recolor, and a basic green-screen removal; the bottom row shows a tuned green-screen with spill control, a stacked Recolor with one effect disabled, and a batch-applied Recolor.](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-colors-replace) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. CE.SDK exposes two color-replacement effects through the engine's effect stack. The **Recolor** effect swaps pixels matching a source color with a target color while preserving image detail. The **Green Screen** effect makes pixels matching a source color transparent, useful for removing solid-color backgrounds. Both effects attach to graphic blocks via `appendEffect(_:effectID:)` and configure colors and tolerance through the standard property API. ## Setting Up a Scene The examples below all attach effects to graphic blocks with image fills. Create a scene with a page that will host the image blocks. ```swift highlight-colorsReplace-setup let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 450) try engine.block.appendChild(to: scene, child: page) let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") ``` ## Creating and Applying Recolor Effects The Recolor effect swaps one color for another throughout an image. Create the effect with `engine.block.createEffect(.recolor)`, set the source color via `effect/recolor/fromColor`, set the target color via `effect/recolor/toColor`, and attach the effect to a graphic block. ```swift highlight-colorsReplace-createRecolor // Create a Recolor effect that swaps red pixels for blue, then attach it to // an image block using `appendEffect`. let recolorBlock = try engine.block.create(.graphic) try engine.block.setShape(recolorBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(recolorBlock, value: 50) try engine.block.setPositionY(recolorBlock, value: 50) try engine.block.setWidth(recolorBlock, value: 200) try engine.block.setHeight(recolorBlock, value: 150) try engine.block.appendChild(to: page, child: recolorBlock) let recolorFill = try engine.block.createFill(.image) try engine.block.setURL(recolorFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(recolorBlock, fill: recolorFill) let recolorEffect = try engine.block.createEffect(.recolor) try engine.block.setColor( recolorEffect, property: "effect/recolor/fromColor", color: .rgba(r: 1, g: 0, b: 0, a: 1), ) try engine.block.setColor( recolorEffect, property: "effect/recolor/toColor", color: .rgba(r: 0, g: 0.5, b: 1, a: 1), ) try engine.block.appendEffect(recolorBlock, effectID: recolorEffect) ``` Pixels close to `fromColor` are remapped to `toColor`. Color values use the `Color.rgba(r:g:b:a:)` constructor with components in the `0...1` range. The alpha channel of `fromColor` is ignored — only the RGB triplet is compared against image pixels. ## Tuning Recolor Tolerance The Recolor effect exposes three numeric properties that control which pixels are affected and how the recolor transition blends. All three accept values in the `0...1` range. ```swift highlight-colorsReplace-configureRecolor let tolerancesEffect = try engine.block.createEffect(.recolor) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/colorMatch", value: 0.3) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/brightnessMatch", value: 0.2) try engine.block.setFloat(tolerancesEffect, property: "effect/recolor/smoothness", value: 0.1) ``` | Property | Default | Purpose | | --- | --- | --- | | `effect/recolor/colorMatch` | `0.4` | Threshold between the source color and the from color. Lower values match a narrower color range, higher values match a broader range. | | `effect/recolor/brightnessMatch` | `1.0` | Weight of brightness when computing similarity. Lower values let the effect reach pixels of the same hue but different brightness. | | `effect/recolor/smoothness` | `0.08` | Rate at which the color transition increases beyond the match threshold. Higher values produce softer edges. | Use `setFloat(_:property:value:)` to write each tolerance. ## Removing Backgrounds with Green Screen The Green Screen effect replaces pixels matching a chosen color with transparency. Create the effect with `engine.block.createEffect(.greenScreen)`, set the color to remove via `effect/green_screen/fromColor`, and append it to a graphic block. ```swift highlight-colorsReplace-createGreenScreen // Create a Green Screen effect. `fromColor` picks the color to remove; any // pixel close enough to that color becomes transparent. let greenScreenBlock = try engine.block.create(.graphic) try engine.block.setShape(greenScreenBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(greenScreenBlock, value: 550) try engine.block.setPositionY(greenScreenBlock, value: 50) try engine.block.setWidth(greenScreenBlock, value: 200) try engine.block.setHeight(greenScreenBlock, value: 150) try engine.block.appendChild(to: page, child: greenScreenBlock) let greenScreenFill = try engine.block.createFill(.image) try engine.block.setURL(greenScreenFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(greenScreenBlock, fill: greenScreenFill) let greenScreenEffect = try engine.block.createEffect(.greenScreen) try engine.block.setColor( greenScreenEffect, property: "effect/green_screen/fromColor", color: .rgba(r: 0, g: 1, b: 0, a: 1), ) try engine.block.appendEffect(greenScreenBlock, effectID: greenScreenEffect) ``` This works best on images with solid, uniform background colors (chroma-key shoots, product photography on flat backdrops). The alpha channel of `fromColor` is ignored. ## Fine-Tuning Green Screen Removal The Green Screen effect exposes three numeric properties that control matching tolerance, edge feathering, and color spill suppression. ```swift highlight-colorsReplace-configureGreenScreen let spillEffect = try engine.block.createEffect(.greenScreen) try engine.block.setFloat(spillEffect, property: "effect/green_screen/colorMatch", value: 0.4) try engine.block.setFloat(spillEffect, property: "effect/green_screen/smoothness", value: 0.2) try engine.block.setFloat(spillEffect, property: "effect/green_screen/spill", value: 0.5) ``` | Property | Default | Purpose | | --- | --- | --- | | `effect/green_screen/colorMatch` | `0.4` | Threshold between the source color and the from color. Higher values capture broader color variations in the background. | | `effect/green_screen/smoothness` | `0.08` | Rate at which the transparency transition increases beyond the match threshold. Higher values feather the matte edge. | | `effect/green_screen/spill` | `0.0` | Desaturates the source color across the image to reduce color bleed from the removed background onto subject edges. | ## Managing Multiple Effects A graphic block carries an effect stack — `appendEffect(_:effectID:)` adds an effect to the end of the stack, and effects are applied in order. Retrieve the current stack with `getEffects(_:)`, toggle individual entries with `setEffectEnabled(effectID:enabled:)`, and query each one's state with `isEffectEnabled(effectID:)`. ```swift highlight-colorsReplace-manageEffects // Stack multiple Recolor effects on a single block, then toggle individual // entries with `setEffectEnabled` without removing them from the stack. let stackedBlock = try engine.block.create(.graphic) try engine.block.setShape(stackedBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(stackedBlock, value: 300) try engine.block.setPositionY(stackedBlock, value: 250) try engine.block.setWidth(stackedBlock, value: 200) try engine.block.setHeight(stackedBlock, value: 150) try engine.block.appendChild(to: page, child: stackedBlock) let stackedFill = try engine.block.createFill(.image) try engine.block.setURL(stackedFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(stackedBlock, fill: stackedFill) let redToBlue = try engine.block.createEffect(.recolor) try engine.block.setColor(redToBlue, property: "effect/recolor/fromColor", color: .rgba(r: 1, g: 0, b: 0, a: 1)) try engine.block.setColor(redToBlue, property: "effect/recolor/toColor", color: .rgba(r: 0, g: 0, b: 1, a: 1)) try engine.block.appendEffect(stackedBlock, effectID: redToBlue) let greenToOrange = try engine.block.createEffect(.recolor) try engine.block.setColor(greenToOrange, property: "effect/recolor/fromColor", color: .rgba(r: 0, g: 1, b: 0, a: 1)) try engine.block.setColor(greenToOrange, property: "effect/recolor/toColor", color: .rgba(r: 1, g: 0.5, b: 0, a: 1)) try engine.block.appendEffect(stackedBlock, effectID: greenToOrange) let stackedEffects = try engine.block.getEffects(stackedBlock) print("Number of effects: \(stackedEffects.count)") // 2 try engine.block.setEffectEnabled(effectID: stackedEffects[0], enabled: false) let isEnabled = try engine.block.isEffectEnabled(effectID: stackedEffects[0]) print("First effect enabled: \(isEnabled)") // false ``` Disabling an effect with `setEffectEnabled(...)` leaves it in the stack so you can re-enable it later — useful for before/after comparisons and for pausing expensive effects during interactive editing. ## Batch Processing Multiple Images `engine.block.find(byType: .graphic)` returns every graphic block in the scene. Combined with `getEffects(_:)`, you can apply a consistent Recolor configuration to every image block that does not already carry an effect. ```swift highlight-colorsReplace-batchProcessing // Apply a consistent Recolor effect to every graphic block in the scene. // Skip blocks that already carry an effect so existing work isn't overwritten. let batchBlock = try engine.block.create(.graphic) try engine.block.setShape(batchBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(batchBlock, value: 550) try engine.block.setPositionY(batchBlock, value: 250) try engine.block.setWidth(batchBlock, value: 200) try engine.block.setHeight(batchBlock, value: 150) try engine.block.appendChild(to: page, child: batchBlock) let batchFill = try engine.block.createFill(.image) try engine.block.setURL(batchFill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(batchBlock, fill: batchFill) let allGraphicBlocks = try engine.block.find(byType: .graphic) for blockID in allGraphicBlocks { if try engine.block.getEffects(blockID).isEmpty == false { continue } let batchRecolor = try engine.block.createEffect(.recolor) try engine.block.setColor( batchRecolor, property: "effect/recolor/fromColor", color: .rgba(r: 0.8, g: 0.7, b: 0.6, a: 1), ) try engine.block.setColor( batchRecolor, property: "effect/recolor/toColor", color: .rgba(r: 0.6, g: 0.7, b: 0.9, a: 1), ) try engine.block.setFloat(batchRecolor, property: "effect/recolor/colorMatch", value: 0.25) try engine.block.appendEffect(blockID, effectID: batchRecolor) } ``` This pattern is useful for generating product-color variations across a catalog and for applying brand-consistent color corrections to a batch of imported images. ## Troubleshooting **Effect not visible** - Verify the effect is enabled with `isEffectEnabled(effectID:)`. - Check that the effect is attached to the intended graphic block with `getEffects(_:)`. - Confirm the block accepts effects with `supportsEffects(_:)` — graphic blocks (including image fills) and pages carry an effect stack; text blocks do not. **Wrong colors being replaced** - Decrease `colorMatch` for stricter matching, or increase it to capture lighting variations and JPEG compression artifacts. - For Recolor, decrease `brightnessMatch` so pixels of the same hue but different brightness are also affected. **Harsh edges or artifacts** - Increase `smoothness` to blend the transition more gradually. - For Green Screen, raise `spill` to desaturate residual background color around subject edges. **Performance during interactive editing** - Use `setEffectEnabled(effectID:enabled: false)` to pause heavy effects between user interactions instead of removing and re-adding them. - Limit the depth of the effect stack on a single block when targeting real-time playback. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.createEffect(_:)` | Create a new effect block. Pass `.recolor` or `.greenScreen` for color replacement. | | `engine.block.appendEffect(_:effectID:)` | Attach an effect to the end of a block's effect stack. | | `engine.block.insertEffect(_:effectID:index:)` | Insert an effect at a specific position in the stack. | | `engine.block.removeEffect(_:index:)` | Remove an effect from a block by stack index. | | `engine.block.getEffects(_:)` | Get all effects currently attached to a block. | | `engine.block.supportsEffects(_:)` | Check whether a block carries an effect stack. | | `engine.block.setColor(_:property:color:)` | Set a color property on an effect, such as `fromColor` or `toColor`. | | `engine.block.getColor(_:property:)` | Read a color property from an effect. | | `engine.block.setFloat(_:property:value:)` | Set a numeric property (tolerance, smoothness, spill). | | `engine.block.getFloat(_:property:)` | Read a numeric property from an effect. | | `engine.block.setEffectEnabled(effectID:enabled:)` | Enable or disable an effect without removing it from the stack. | | `engine.block.isEffectEnabled(effectID:)` | Check whether an effect is currently enabled. | | `engine.block.find(byType:)` | Find all blocks of a given `DesignBlockType` — pass `.graphic` to locate every graphic block in the scene. | ### Properties | Property | Type | Description | | --- | --- | --- | | `effect/recolor/fromColor` | `Color` | The source color the Recolor effect matches against. Alpha is ignored. | | `effect/recolor/toColor` | `Color` | The target color matched pixels are remapped to. | | `effect/recolor/colorMatch` | `Float` | Match threshold against `fromColor`. Range `0...1`. | | `effect/recolor/brightnessMatch` | `Float` | Weight of brightness in the similarity calculation. Range `0...1`. | | `effect/recolor/smoothness` | `Float` | Transition softness beyond the match threshold. Range `0...1`. | | `effect/green_screen/fromColor` | `Color` | The color the Green Screen effect makes transparent. Alpha is ignored. | | `effect/green_screen/colorMatch` | `Float` | Match threshold against `fromColor`. Range `0...1`. | | `effect/green_screen/smoothness` | `Float` | Edge feathering on the transparency matte. Range `0...1`. | | `effect/green_screen/spill` | `Float` | Desaturation of the source color to reduce spill. Range `0...1`. | ## Next Steps - [Apply Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) — Explore the broader catalog of filters, blurs, and effects available on graphic blocks. - [Export Designs](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) — Save your color-replaced images in PNG, JPEG, PDF, and other formats. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Transform" description: "Crop, resize, rotate, scale, or flip images using CE.SDK's built-in transformation tools." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/) --- --- ## Related Pages - [Move Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/move-818dd9/) - Position images precisely on the canvas using absolute or percentage-based coordinates. - [Crop Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/crop-f67a47/) - Cut out specific areas of an image to focus on key content or change aspect ratio. - [Rotate](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/rotate-5f39c9/) - Documentation for Rotate - [Resize](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/resize-407242/) - Change image dimensions by setting explicit width and height values using absolute units, percentage-based sizing, or auto-sizing modes. - [Scale](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/scale-ebe367/) - Resize images uniformly in your app. - [Flip Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/flip-035e9f/) - Flip images horizontally or vertically, or mirror their content inside a crop frame. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Crop Images" description: "Cut out specific areas of an image to focus on key content or change aspect ratio." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/crop-f67a47/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/) > [Crop](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/crop-f67a47/) --- Cropping images is a fundamental editing operation that helps you frame your subject, remove unwanted elements, or prepare visuals for specific formats. With the CreativeEditor SDK (CE.SDK) for iOS, you can crop images either using the built-in user interface or programmatically via the engine API. This guide covers both methods and explains how to apply constraints such as fixed aspect ratios or exact dimensions when using templates. ## What You’ll Learn - How to enable and use the pre-built crop UI. - How to query whether a block supports cropping. - How to adjust crop via helper methods or properties for scale, translation, rotation, and flip. - How to reset a crop and how to fill the frame programmatically. - How to chain crop transformations ## When To Use It Use the built-in UI when end-users should adjust the crop visually. Use the programmatic API when you need: - Automation. - To enforce brand layouts. - To drive cropping from templates or data. ## Using the Built-In Crop UI CE.SDK provides a user-friendly cropping tool in its default UI. Users can interactively: - **Adjust** crop areas. - **Select** preset aspect ratios. - Apply changes with **real-time** feedback. This makes it easy to support social media presets or maintain brand consistency. ![Crop tool appears when a selected image allows cropping.](assets/ios-crop-tool-161.png) ### User Interaction Workflow 1. **Select the image** you want to crop. 2. **Tap the crop icon** in the editor toolbar. 3. Drag the corners or edges, **adjusting** the crop area. 4. **Use the tools** to crop flip, rotate, resize or, to reset the image. 5. **Close the Sheet** to finalize the crop. ![An image that has been scale cropped and rotated slightly showing the cropped and original image.](assets/ios-ui-crop-workflow-161.png) The cropped image appears in your project, but the underlying original image and crop values are preserved even when you rotate or resize the cropped image. ### Enable and Configure Crop Tool The default editor UI allows cropping. When you are creating your own UI or custom toolbars, you can configure editing behavior. To ensure the crop tool is available in the UI, make sure you include it in your app in either: - The dock configuration - The quick actions ```swift try engine.editor.setSettingBool("doubleClickToCropEnabled", value: true) try engine.editor.setSettingBool("controlGizmo/showCropHandles", value: true) try engine.editor.setSettingBool("controlGizmo/showCropScaleHandles", value: true) ``` The cropping handles are only available when a selected block has a fill of type `.image`. Otherwise setting the edit mode of the `engine.editor` to `.crop` has no effect. ### Canvas vs. Prebuilt Editors The CE.SDK offers two UI paths: - Canvas (all platforms) view shows built-in crop controls for selected image blocks when the crop gizmos are enabled. No extra wiring is required. - Prebuilt Editors (iOS only) such as the Design Editor include an Inspector Bar with a Crop button. You can ensure the button is present or customize it using: - The `inspectorBar.items` or `inspectorBar.modify` builder methods - The predefined `InspectorBar.Buttons.crop()` item ### Crop Presets CE.SDK has built-in crop presets as part of the default asset sources. You can also provide your own preset set by adding to the default source or serving your own source. ```swift let engine = Engine() let cropPresetsURL = URL( string: "https://cdn.img.ly/packages/imgly/cesdk-swift/1.86.2/assets/ly.img.crop.presets/content.json" )! try await engine.asset.addLocalAssetSourceFromJSON(cropPresetsURL) ``` To create your own preset definitions, you can serve a custom sets. Define presets using JSON in the `ly.img.crop.presets` asset source. Common types you can publish include: - FixedSize (absolute width/height in a unit) - FixedAspectRatio (ratio only) - FreeAspectRatio (unconstrained) Below is an example (FixedSize): ```json { "id": "page-sizes-instagram-square", "label": { "en": "Square (1080×1080)" }, "type": "FixedSize", "width": 1080, "height": 1080, "designUnit": "Pixel", "groups": ["instagram"] } ``` Publish your JSON with other served assets [from your server](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) and register that source. ## Programmatic Cropping Programmatic cropping gives you complete control over: - Image boundaries - Dimensions - Integration with other transformations like rotation or flipping. This is useful for: - Automation - Predefined layouts - Server-synced workflows. When you initially create a fill to insert an image into a block, the engine: 1. Centers the image in the block. 2. Crops any dimension that doesn't match. For example: when a block with dimensions of 400.0 × 400.0 is filled with an image that is 600.0 × 500.0, there will be horizontal cropping. When working with cropping using code, it’s important to remember that you are modifying the scale, translation, rotation, etc. of the underlying image. The examples below always adjust the x and y values equally. This isn’t required, but adjusting them unequally can distort the image, which might be just what you want. ### Verify Crop Permission Before your code can apply any cropping, it should first verify that a block currently supports cropping. ```swift let canCrop = try engine.block.supportsCrop(imageBlock) ``` ### Reset Crop When an image is initially placed into a block it will get crop scale and crop translation values. Resetting the crop will return the image to the original values. ![Image with no additional crop applied shown in crop mode](../mobile-assets/crop-example-1.png) This is a block (called `imageBlock` in the example code) with dimensions of 400 × 400 filled with an image that has dimensions of 600 × 530. The image has slight scaling and translation applied so that it fills the block evenly. At any time, the code can execute the reset crop command to return it to this stage. ```swift try engine.block.resetCrop(imageBlock) ``` ### Crop Translation The translation values adjust the placement of the origin point of an image. You can read and change the values. They’re not pixel units or centimeters, they’re scaled percentages. An image that has its origin point at the origin point of the crop block has a translation value of 0.0 for x and y. ![Image crop translated one quarter of it's width to the right](../mobile-assets/crop-example-5.png) ```swift try engine.block.setCropTranslationX(imageBlock, translationX: 0.250) ``` This image has had its translation in the x direction set to 0.25. That moved the image one quarter of its width to the right. Setting the value to -0.25 would change the offset of the origin to the left. These are absolute values. Setting the x value to 0.25 and then setting it to -0.25 does not move the image to an offset of 0.0. There is a `setCropTranslationY(_ id: DesignBlockID, translationY: Float)` function to adjust the translation of the image in the vertical direction. Negative values move the image up and positive values move the image down. To read the current crop translation values you can use the convenience getters for the x and y values. ```swift let currentX = try engine.block.getCropTranslationX(imageBlock) let currentY = try engine.block.getCropTranslationY(imageBlock) ``` ### Crop Scale The scale values adjust the height and width of the underlying image. Values larger than 1.0 will make the image larger while values less than 1.0 make the image smaller. Unless the image also has offsetting translation applied, the center of the image will move. ![Image crop scaled by 1.5 with no corresponding translation adjustment](../mobile-assets/crop-example-6.png) This image has been scaled by 1.5 in the x and y directions, but the origin point has not been translated. So, the center of the image has moved. ```swift try engine.block.setCropScaleX(imageBlock, scaleX: 1.50) try engine.block.setCropScaleY(imageBlock, scaleY: 1.50) ``` To read the current crop scale values you can use the convenience getters for the x and y values. ```swift let currentX = try engine.block.getCropScaleX(imageBlock) let currentY = try engine.block.getCropScaleY(imageBlock) ``` ### Crop Rotate The same as when rotating blocks, the crop rotation function uses radians: - Positive values rotate clockwise. - Negative values rotate counter clockwise. The image rotates around its center. ![Image crop rotated by pi/4 or 45 degrees](../mobile-assets/crop-example-7.png) ```swift try engine.block.setCropRotation(block, rotation: .pi / 4.0) ``` For working with radians, Swift has a constant defined for pi. It can be used as either `Float.pi` or `Double.pi`. Because the `setCropRotation` function takes a `Float` for the rotation value, you can use `.pi` and Swift will infer the correct type. ### Crop to Scale Ratio To center crop an image, you can use the scale ratio. This will adjust the x and y scales of the image evenly, and adjust the translation to keep it centered. ![Image cropped using the scale ratio to remain centered](../mobile-assets/crop-example-2.png) This image has been scaled by 2.0 in the x and y directions. It's translation has been adjusted by -0.5 in the x and y directions to keep the image centered. ```swift try engine.block.setCropScaleRatio(imageBlock, scaleRatio: 2.0) ``` Using the crop scale ratio function is the same as calling the translation and scale functions, but in one line. ```swift try engine.block.setCropScaleX(block, scaleX: 2.0) try engine.block.setCropScaleY(block, scaleY: 2.0) try engine.block.setCropTranslationX(block, translationX: -0.5) try engine.block.setCropTranslationY(block, translationY: -0.5) ``` ### Crop to Fixed Dimensions The crop frame is the block itself, so cropping to exact dimensions means resizing the block. Use fixed dimensions when you want the visible region to have an exact size, such as when either: - Matching a specified bounding box. - Recreating a design template. ```swift try engine.block.setWidth(imageBlock, value: 300) try engine.block.setHeight(imageBlock, value: 300) try engine.block.adjustCropToFillFrame(imageBlock, minScaleRatio: 1.0) ``` The result of the preceding code is an image cropped to a 300 × 300 square in the scene's design unit. Calling `adjustCropToFillFrame(_:minScaleRatio:)` afterwards scales and repositions the image so it covers the resized frame. Use the crop translation functions described above to choose which part of the image stays visible. Alternatively, pass `maintainCrop: true` to `setWidth(_:value:maintainCrop:)` and `setHeight(_:value:maintainCrop:)` to have the engine adjust the crop values automatically while resizing. ### Crop to Aspect Ratio When you want to target a specific aspect ratio, such as 4:5 or 16:9, calculate the new frame height from the current frame width and resize the block accordingly. ```swift let targetRatio: Float = 4.0 / 5.0 let frameWidth = try engine.block.getFrameWidth(imageBlock) let newHeight = frameWidth / targetRatio try engine.block.setHeight(imageBlock, value: newHeight) try engine.block.adjustCropToFillFrame(imageBlock, minScaleRatio: 1.0) ``` The preceding code crops an image to a portrait 4:5 ratio while keeping its current width. To keep users from changing the ratio afterwards in the crop UI, lock it: ```swift try engine.block.setCropAspectRatioLocked(imageBlock, locked: true) ``` ### Chained Crops Crop operations can be chained together. The order of the chaining impacts the final image. ![Image cropped and rotated](../mobile-assets/crop-example-3.png) ```swift try engine.block.setCropScaleRatio(block, scaleRatio: 2.0) try engine.block.setCropRotation(block, rotation: .pi / 3.0) ``` ![Image rotated first and then scaled](../mobile-assets/crop-example-4.png) ```swift try engine.block.setCropRotation(block, rotation: .pi / 3.0) try engine.block.setCropScaleRatio(block, scaleRatio: 2.0) ``` ### Flipping the Crop There are two functions for crop flipping the image. One for horizontal and one for vertical. They each flip the image along its center. ![Image crop flipped vertically](../mobile-assets/crop-example-8.png) ```swift try engine.block.flipCropVertical(imageBlock) try engine.block.flipCropHorizontal(imageBlock) ``` The image will be crop flipped every time the function gets called. So calling the function an even number of times will return the image to its original orientation. ### Filling the Frame When the various crop operations cause the background of the crop block to be displayed, such as in the **Crop Translation** example above, the function ```swift try engine.block.adjustCropToFillFrame(imageBlock, minScaleRatio: 1.0) ``` adjusts these values: - Translation values - Scale values This way, the entire crop block is filled. This is not the same as resetting the crop. ## Legacy vs. Modern Presets Earlier versions of the SDK used editor keys such as `ui/crop/aspectRatios` to define ratio lists. These were deprecated and replaced by an asset source for presets, `ly.img.crop.presets`. When you encounter legacy configuration examples, migrate them by creating or editing the corresponding preset JSON objects instead. ## Relationship to Video Crop Cropping tools behave the same for still images as for video frames. Video crops also interact with clip trimming and frame bounds within the timeline. ## Troubleshooting **❌ Crop handles don’t appear**: - Ensure the selected block’s fill is an image. - `controlGizmo/showCropHandles` editor setting should be `true`. **❌ Crop ignored**: - Confirm `supportsCrop(_:)` returns `true` for the block. **❌ Background visible after edits**: - Call `adjustCropToFillFrame(_:minScaleRatio:)` to restore coverage. **❌ Cropped image appears distorted**: - Check `setCropScaleX`, `setCropScaleY` values are as expected. - Use `setCropScaleRatio` for a uniform scale. ## Next Steps Now that you’ve seen how to work with cropping your images, some other topics to explore are: - Other [image transformations](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/) such as rotate, resize, scale, and flip. - Customize the [Dock](#broken-link-cb916c) or [Inspector Bar](#broken-link-8ca1cd) to add or replace the crop button or provide a custom preset toolbar. - Constrain who can crop using scopes and template rules to [lock a template](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/). --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Flip Images" description: "Flip images horizontally or vertically, or mirror their content inside a crop frame." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/flip-035e9f/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/) > [Flip](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/flip-035e9f/) --- Use CE.SDK to flip or mirror image and video elements horizontally or vertically in your app. This guide covers block-level and crop-level flipping, batch operations, mirror effects, and scope-based permissions. ## What you'll learn - Flip an image horizontally or vertically. - Understand the difference between block flip and content crop flip. - Use both dedicated methods and property-based approaches. - Flip multiple elements together. - Create mirrored or reflection effects. - Protect templates by locking flip permissions. ## When to use Flipping is helpful when: - Mirroring product or model images for layout consistency. - Creating stylistic reflections or symmetrical designs. - Adjusting orientation in right-to-left layouts. - Correcting flipped camera footage. *** ## Flip Types: Block vs. Crop There are two kinds of flips in CE.SDK: | Flip type | Methods | What is mirrored | When to use | | ---------- | ---------------------------------------: | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | Block flip | `setFlipHorizontal`, `setFlipVertical` | Entire block — including borders, effects, and overlays; changes how the block is rendered on the canvas. | Layout corrections or composition changes | | Crop flip | `flipCropHorizontal`, `flipCropVertical` | Only the content inside the crop frame; block layout and dimensions remain unchanged. | Adjust underlying image/video orientation without affecting placement | Use **block flips** for layout corrections or composition changes, and **crop flips** to adjust underlying image or video orientation without affecting placement. ## Flip horizontally or vertically Use the `flip/horizontal` and `flip/vertical` properties to control mirroring. They are boolean properties and have dedicated helper functions defined. All flips are around the center point of a block. ```swift try engine.block.setFlipVertical(imageBlock, flip: true) try engine.block.setFlipHorizontal(imageBlock, flip: true) ``` To determine if a block has been flipped you can query the properties or use helper functions. ```swift let isFlippedHorizontally = try engine.block.getFlipHorizontal(imageBlock) let isFlippedVertically = try engine.block.getFlipVertical(imageBlock) ``` ### Property-Based Approach In addition to convenience methods, you can use the property API for dynamic or batch operations. Blocks have `"flip/horizontal"` and `"flip/vertical"` Boolean properties. ```swift try engine.block.setBool(imageBlock, property: "flip/horizontal", value: true) try engine.block.setBool(imageBlock, property: "flip/vertical", value: true) ``` | Approach | When to use | Notes | | -------------------------: | ---------------------------------------------------------- | ------------------------------------------------- | | Dedicated helper functions | Type safety when writing explicit flip operations | Prefer for explicit calls — safer, clearer API | | Property-based approach | Flexible key-path manipulation in batch scripts or tooling | Better for dynamic/bulk updates; less type safety | ## Flip Multiple Elements Together Group blocks and apply flip to the group: ```swift let groupId = try engine.block.group([imageId, textId]) try engine.block.setFlipHorizontal(groupId, flip: true) ``` While respecting scope permissions, you can also: 1. Iterate over all blocks of a type. 2. Flip each one individually. ```swift let blocks = try engine.block.find(byType: .graphic) for id in blocks { if try engine.block.isAllowedByScope(id, key: "layer/flip") { try engine.block.setFlipHorizontal(id, flip: true) } } ``` ![Items flipped individually and as a group](assets/flip-group-160.jpg) The preceding code: - Shows the original composition on the left. - Flips each item individually in the center composition. - Groups first, then flips the group for the composition on the right. ## To Remove Any Flip Applied If you want to remove the flip, set the property to false. ```swift try engine.block.setFlipVertical(block, flip: false) ``` Applying the flip multiple times doesn’t flip the image back to its original orientation. This code results in a flipped block. ```swift try engine.block.setFlipVertical(block, flip: true) try engine.block.setFlipVertical(block, flip: true) ``` ## Flip Crop Flips Content Only When you need to flip the image inside its crop region without changing the block’s placement: ```swift try engine.block.flipCropHorizontal(imageBlock) try engine.block.flipCropVertical(imageBlock) ``` These operations invert the crop’s translation and scale values, producing a mirror effect within the same bounding box. Use them for correcting camera orientation or stylized reflections without shifting the layout. ## Create Mirror and Reflection Effects You can simulate reflections or mirrored designs by duplicating, flipping, and adjusting opacity and position: ```swift let mirrored = try engine.block.duplicate(original) try engine.block.setFlipVertical(mirrored, flip: true) try engine.block.setOpacity(mirrored, value: 0.5) try engine.block.setPositionY(mirrored, value: 200) ``` ![Image mirrored using preceding code](assets/flip-mirror-160.png) > **Note:** Try combining vertical flips with gradients or masks for realistic water or > glass reflections. ## Lock or constrain flipping (optional) When building templates, you might want to lock flipping to protect the layout: ```swift try engine.block.setScopeEnabled(block, key: "layer/flip", enabled: false) ``` You can also disable all transformations by locking, this is regardless of working with a template. ```swift try engine.block.setTransformLocked(block, locked: true) ``` ## Troubleshooting | Issue | Solution | | ------------------------------- | ------------------------------------------------------------------- | | Flipping doesn’t apply visually | Confirm image is rendered and loaded | | Image flips unexpectedly | Check that flipping is not being overridden by grouped parent block | | User can still flip in editor | Use "layer/flip" constraint to prevent this | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Move Images" description: "Position images precisely on the canvas using absolute or percentage-based coordinates." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/move-818dd9/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/) > [Move](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/move-818dd9/) --- ```swift file=@cesdk_swift_examples/engine-guides-edit-image-transform-move/MoveImages.swift reference-only import Foundation import IMGLYEngine @MainActor func moveImages(engine: Engine) async throws { // Demo scaffolding: a scene with two image blocks on a single page so we can // demonstrate every positioning API against real, renderable content. let scene = try engine.scene.create() let baseURL = try engine.guidesBaseURL let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let imageBlock = try makeImageBlock( engine: engine, url: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), width: 240, height: 200, ) try engine.block.appendChild(to: page, child: imageBlock) let secondImage = try makeImageBlock( engine: engine, url: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), width: 240, height: 200, ) try engine.block.appendChild(to: page, child: secondImage) try engine.block.setPositionX(secondImage, value: 460) try engine.block.setPositionY(secondImage, value: 350) try engine.block.setPositionX(imageBlock, value: 150) try engine.block.setPositionY(imageBlock, value: 100) try await engine.captureGuide(page, label: "after-set-position") let xPosition = try engine.block.getPositionX(imageBlock) let yPosition = try engine.block.getPositionY(imageBlock) _ = (xPosition, yPosition) try engine.block.setPositionXMode(imageBlock, mode: .percent) try engine.block.setPositionYMode(imageBlock, mode: .percent) try engine.block.setPositionX(imageBlock, value: 0.5) try engine.block.setPositionY(imageBlock, value: 0.5) try await engine.captureGuide(page, label: "after-percent") let xMode = try engine.block.getPositionXMode(imageBlock) let yMode = try engine.block.getPositionYMode(imageBlock) _ = (xMode, yMode) let currentX = try engine.block.getPositionX(imageBlock) try engine.block.setPositionX(imageBlock, value: currentX + 0.05) // Switch back to absolute mode before grouping so the group below uses pixel // coordinates. The reader sees the explicit mode flip once and then keeps // working in absolute units. try engine.block.setPositionXMode(imageBlock, mode: .absolute) try engine.block.setPositionYMode(imageBlock, mode: .absolute) try engine.block.setPositionX(imageBlock, value: 120) try engine.block.setPositionY(imageBlock, value: 80) try engine.block.setPositionX(secondImage, value: 420) try engine.block.setPositionY(secondImage, value: 80) if try engine.block.isGroupable([imageBlock, secondImage]) { let group = try engine.block.group([imageBlock, secondImage]) try engine.block.setPositionX(group, value: 80) try engine.block.setPositionY(group, value: 200) } try await engine.captureGuide(page, label: "hero") try engine.block.setTransformLocked(imageBlock, locked: true) } @MainActor private func makeImageBlock( engine: Engine, url: URL, width: Float, height: Float, ) throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: url) try engine.block.setFill(block, fill: fill) try engine.block.setContentFillMode(block, mode: .cover) try engine.block.setWidth(block, value: width) try engine.block.setHeight(block, value: height) return block } ``` Position images on the canvas using absolute pixel coordinates or percentage-based positioning for responsive layouts. ![Two images repositioned and grouped on a single page after switching between absolute and percentage position modes.](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-edit-image-transform-move) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Position images on the canvas using coordinates that start at the top-left corner `(0, 0)`. X increases to the right, Y increases downward. Values are relative to the parent block, which simplifies nested layouts. ## Position Coordinates Coordinates originate at the top-left `(0, 0)` of the parent container. Use **absolute** mode for fixed pixel values or **percentage** mode (`0.0` to `1.0`) for responsive layouts that adapt to parent size changes. ## Positioning Images Set the image's coordinates with `setPositionX(_:value:)` and `setPositionY(_:value:)`. Both setters take a `Float` interpreted in the current position mode — absolute design units when the mode is `.absolute` (pixels on a default `DesignUnit.px` scene), or `0.0`–`1.0` fractions of the parent's size when the mode is `.percent`. ```swift highlight-moveImages-setPosition try engine.block.setPositionX(imageBlock, value: 150) try engine.block.setPositionY(imageBlock, value: 100) ``` ## Getting Current Position Read the current coordinates with `getPositionX(_:)` and `getPositionY(_:)`. Each returns a `Float` interpreted in the block's current position mode (absolute pixels or a `0.0`–`1.0` fraction). ```swift highlight-moveImages-getPosition let xPosition = try engine.block.getPositionX(imageBlock) let yPosition = try engine.block.getPositionY(imageBlock) ``` ## Configuring Position Modes Each axis carries its own mode. Use `setPositionXMode(_:mode:)` and `setPositionYMode(_:mode:)` with `PositionMode.absolute` or `PositionMode.percent`. Read the current mode back with `getPositionXMode(_:)` and `getPositionYMode(_:)`. ```swift highlight-moveImages-getMode let xMode = try engine.block.getPositionXMode(imageBlock) let yMode = try engine.block.getPositionYMode(imageBlock) ``` The Percentage Positioning section below shows how to flip both axes to `.percent` and use fractional values. ## Percentage Positioning Switch the modes to `.percent` and use values from `0.0` to `1.0`. The example centers the image on the page by placing its anchor at the midpoint of the parent on both axes. ```swift highlight-moveImages-percent try engine.block.setPositionXMode(imageBlock, mode: .percent) try engine.block.setPositionYMode(imageBlock, mode: .percent) try engine.block.setPositionX(imageBlock, value: 0.5) try engine.block.setPositionY(imageBlock, value: 0.5) ``` Percentage positioning adapts automatically when the parent block's dimensions change, keeping the image's relative placement stable across responsive scenes. ## Relative Positioning Move an image relative to its current position by reading the current coordinate and adding an offset. Because position values are interpreted in the current mode, the offset uses the same units — pixels in `.absolute` mode, fractions in `.percent` mode. ```swift highlight-moveImages-relative let currentX = try engine.block.getPositionX(imageBlock) try engine.block.setPositionX(imageBlock, value: currentX + 0.05) ``` ## Positioning Groups To move several images together while preserving their relative positions, group them and move the group block. Confirm the blocks can be grouped first with `isGroupable(_:)`, then call `group(_:)` and position the returned group ID like any other block. ```swift highlight-moveImages-group if try engine.block.isGroupable([imageBlock, secondImage]) { let group = try engine.block.group([imageBlock, secondImage]) try engine.block.setPositionX(group, value: 80) try engine.block.setPositionY(group, value: 200) } ``` ## Locking Transforms Lock move, scale, and rotate on a block with `setTransformLocked(_:locked:)`. Subsequent calls to `setPositionX`, `setPositionY`, `setWidth`, `setHeight`, and the matching mode setters throw an `EngineError` whose `catalogCode` is `EngineErrorCode.blockPositionLocked` (or `EngineErrorCode.blockTransformLockedResize`) until the block is unlocked. ```swift highlight-moveImages-transformLock try engine.block.setTransformLocked(imageBlock, locked: true) ``` ## Troubleshooting ### Image Not Moving Check whether transforms are locked using `isTransformLocked(_:)`. Verify the image block exists and that target coordinates fall within the parent's bounds. ### Unexpected Position Values Read the mode with `getPositionXMode(_:)` and `getPositionYMode(_:)`. Pixel values in `.absolute` mode and `0.0`–`1.0` fractions in `.percent` mode look very different — a value of `0.5` is half a pixel in one and the middle of the parent in the other. ### Positioned Outside Visible Area Confirm the parent block's width and height. Coordinates originate at the top-left, not the center, so a position equal to the parent's size lands the block's anchor flush against the parent's far edge. ### Percentage Positioning Not Responsive Both axes need to be in `.percent` mode for fractional values to be interpreted as percentages. Set the mode on each axis before assigning the percent value, and keep the value between `0.0` and `1.0`. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.setPositionX(_:value:)` | Set the X coordinate of a block | | `engine.block.setPositionY(_:value:)` | Set the Y coordinate of a block | | `engine.block.getPositionX(_:)` | Read the current X coordinate | | `engine.block.getPositionY(_:)` | Read the current Y coordinate | | `engine.block.setPositionXMode(_:mode:)` | Set the position mode for the X axis | | `engine.block.setPositionYMode(_:mode:)` | Set the position mode for the Y axis | | `engine.block.getPositionXMode(_:)` | Read the X axis position mode | | `engine.block.getPositionYMode(_:)` | Read the Y axis position mode | | `engine.block.isGroupable(_:)` | Check whether a set of blocks can be grouped | | `engine.block.group(_:)` | Group blocks into a single movable parent | | `engine.block.setTransformLocked(_:locked:)` | Lock or unlock move, scale, and rotate on a block | | `engine.block.isTransformLocked(_:)` | Read the transform lock state | ### Enums | Enum | Cases | Description | | --- | --- | --- | | `PositionMode` | `.absolute`, `.percent`, `.auto` | How a position value is interpreted on its axis | ## Next Steps - [Lock Content](https://img.ly/docs/cesdk/mac-catalyst/rules/lock-content-9fa727/) — Restrict editing with the scope-based permission system - [Replace Colors](https://img.ly/docs/cesdk/mac-catalyst/edit-image/replace-colors-6ede17/) — Modify image colors programmatically - [Image Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/image-e9cb5c/) — Apply images as fills to shapes - [Gradient Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/gradient-0ff079/) — Create gradient backgrounds and effects - [Crop Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/crop-f67a47/) — Frame your subject and remove unwanted edges --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Resize" description: "Change image dimensions by setting explicit width and height values using absolute units, percentage-based sizing, or auto-sizing modes." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/resize-407242/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/) > [Resize](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/resize-407242/) --- ```swift file=@cesdk_swift_examples/engine-guides-edit-image-transform-resize/ResizeImages.swift reference-only import Foundation import IMGLYEngine @MainActor func resizeImages(engine: Engine) async throws { let scene = try engine.scene.create() let baseURL = try engine.guidesBaseURL let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let imageBlock = try createImageBlock(engine: engine, page: page, baseURL: baseURL) try engine.editor.setResizeHandlesVisibility(.always) let resizeHandlesVisibility = try engine.editor.getResizeHandlesVisibility() print("Resize handles: \(resizeHandlesVisibility.rawValue)") try engine.block.setWidthMode(imageBlock, mode: .absolute) try engine.block.setHeightMode(imageBlock, mode: .absolute) try engine.block.setWidth(imageBlock, value: 400) try engine.block.setHeight(imageBlock, value: 300) let absoluteWidth = try engine.block.getWidth(imageBlock) let absoluteHeight = try engine.block.getHeight(imageBlock) print("Configured size: \(absoluteWidth) x \(absoluteHeight)") // Center the resized block on the page for the guide's hero image. try engine.block.setPositionX(imageBlock, value: 200) try engine.block.setPositionY(imageBlock, value: 150) try await engine.captureGuide(page, label: "hero") try engine.block.setWidthMode(imageBlock, mode: .percent) try engine.block.setHeightMode(imageBlock, mode: .percent) try engine.block.setWidth(imageBlock, value: 0.5) try engine.block.setHeight(imageBlock, value: 0.5) let percentWidth = try engine.block.getWidth(imageBlock) let widthMode = try engine.block.getWidthMode(imageBlock) print("Configured width: \(percentWidth) (mode is percent: \(widthMode == .percent))") let frameWidth = try engine.block.getFrameWidth(imageBlock) let frameHeight = try engine.block.getFrameHeight(imageBlock) print("Frame size: \(frameWidth) x \(frameHeight)") try engine.block.setContentFillMode(imageBlock, mode: .crop) try engine.block.setWidthMode(imageBlock, mode: .absolute) try engine.block.setHeightMode(imageBlock, mode: .absolute) try engine.block.setWidth(imageBlock, value: 520, maintainCrop: true) try engine.block.setHeight(imageBlock, value: 320, maintainCrop: true) let contentFillMode = try engine.block.getContentFillMode(imageBlock) print("Content fill mode is crop: \(contentFillMode == .crop)") let secondImageBlock = try createImageBlock(engine: engine, page: page, baseURL: baseURL) try engine.block.setPositionX(secondImageBlock, value: 460) let group = try engine.block.group([imageBlock, secondImageBlock]) try engine.block.setWidth(group, value: 600) let groupWidth = try engine.block.getWidth(group) print("Group width: \(groupWidth)") try engine.block.resizeContentAware([page], width: 1080, height: 1080) let pageWidth = try engine.block.getWidth(page) print("Page width after content-aware resize: \(pageWidth)") try engine.editor.setGlobalScope(key: "layer/resize", value: .defer) try engine.block.setScopeEnabled(group, key: "layer/resize", enabled: false) let resizeAllowed = try engine.block.isAllowedByScope(group, key: "layer/resize") print("Resize allowed: \(resizeAllowed)") try engine.block.setTransformLocked(group, locked: true) let locked = try engine.block.isTransformLocked(group) print("Transform locked: \(locked)") } @MainActor private func createImageBlock( engine: Engine, page: DesignBlockID, baseURL: URL, ) throws -> DesignBlockID { let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(imageBlock, value: 320) try engine.block.setHeight(imageBlock, value: 240) try engine.block.setPositionX(imageBlock, value: 120) try engine.block.setPositionY(imageBlock, value: 120) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.appendChild(to: page, child: imageBlock) return imageBlock } ``` Change image dimensions by setting exact width and height values, switching size modes, preserving crop state, or resizing grouped blocks together. ![A sample photo resized to 400 by 300 pixels and centered on the page canvas.](./assets/swift-based.hero.webp) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-edit-image-transform-resize) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Image resizing changes a block's dimensions rather than applying a scale multiplier. Use `engine.block.setWidth(_:value:)` and `engine.block.setHeight(_:value:)` for individual dimensions, and choose a `SizeMode` to control how each value is interpreted. This guide covers resizing image blocks with absolute or percentage sizing, preserving crop state during resize, resizing groups, and locking resize permissions for templates. To grow or shrink an image proportionally instead, see [Scale](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/scale-ebe367/). ## Create an Image Block Create a graphic block with an image fill before applying resize operations: ```swift highlight-resizeImages-createImageBlock @MainActor private func createImageBlock( engine: Engine, page: DesignBlockID, baseURL: URL, ) throws -> DesignBlockID { let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(imageBlock, value: 320) try engine.block.setHeight(imageBlock, value: 240) try engine.block.setPositionX(imageBlock, value: 120) try engine.block.setPositionY(imageBlock, value: 120) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.appendChild(to: page, child: imageBlock) return imageBlock } ``` The resize APIs operate on the block frame. The image fill stays attached to the graphic block and follows the crop and fill mode settings. ## Understanding Size Modes Size values are interpreted in three modes. `SizeMode.absolute` uses fixed design units, `SizeMode.percent` uses parent-relative values from `0.0` to `1.0`, and `SizeMode.auto` lets CE.SDK calculate the size from content where supported. Use `engine.block.getWidth(_:)` and `engine.block.getHeight(_:)` to read the configured values. Use `engine.block.getFrameWidth(_:)` and `engine.block.getFrameHeight(_:)` when you need the calculated layout dimensions after CE.SDK resolves the block. ## Using the Built-In Resize UI When you build an interactive editor with CE.SDK, a selected block displays resize handles on its edges. Control when the non-proportional edge handles appear with the typed resize-handle visibility API: ```swift highlight-resizeImages-resizeHandles try engine.editor.setResizeHandlesVisibility(.always) let resizeHandlesVisibility = try engine.editor.getResizeHandlesVisibility() print("Resize handles: \(resizeHandlesVisibility.rawValue)") ``` `HandleVisibility` accepts `.always`, `.never`, and `.auto`. This setting controls edge-handle visibility only. Programmatic resize APIs and other transform controls still follow the block's scopes and transform lock state. ## Setting Absolute Dimensions Set explicit dimensions by switching both axes to `SizeMode.absolute`, then writing width and height values: ```swift highlight-resizeImages-absoluteSize try engine.block.setWidthMode(imageBlock, mode: .absolute) try engine.block.setHeightMode(imageBlock, mode: .absolute) try engine.block.setWidth(imageBlock, value: 400) try engine.block.setHeight(imageBlock, value: 300) let absoluteWidth = try engine.block.getWidth(imageBlock) let absoluteHeight = try engine.block.getHeight(imageBlock) print("Configured size: \(absoluteWidth) x \(absoluteHeight)") ``` Absolute sizing is the most direct option for fixed layouts, templates, and exports where the target design units are known. ## Percentage Sizing Use percentage mode for responsive sizing. A value of `1.0` means 100 percent of the parent on that axis: ```swift highlight-resizeImages-percentSize try engine.block.setWidthMode(imageBlock, mode: .percent) try engine.block.setHeightMode(imageBlock, mode: .percent) try engine.block.setWidth(imageBlock, value: 0.5) try engine.block.setHeight(imageBlock, value: 0.5) let percentWidth = try engine.block.getWidth(imageBlock) let widthMode = try engine.block.getWidthMode(imageBlock) print("Configured width: \(percentWidth) (mode is percent: \(widthMode == .percent))") ``` Percentage sizing adapts when the parent dimensions change, which makes it useful for reusable templates and generated layouts. ## Getting Frame Dimensions Configured width and height can differ from the rendered frame in percentage and auto modes. Read the frame dimensions when you need the final layout size: ```swift highlight-resizeImages-frameDimensions let frameWidth = try engine.block.getFrameWidth(imageBlock) let frameHeight = try engine.block.getFrameHeight(imageBlock) print("Frame size: \(frameWidth) x \(frameHeight)") ``` Frame dimensions reflect the block's resolved layout, which CE.SDK calculates after size or mode changes. Read them once the engine has processed the update. ## Maintaining Crop During Resize When you resize an image block, you change the image frame. The fill and crop state control how the image content stays framed inside that new size. Pass `maintainCrop: true` when the current crop should stay visually stable while the frame changes: ```swift highlight-resizeImages-maintainCrop try engine.block.setContentFillMode(imageBlock, mode: .crop) try engine.block.setWidthMode(imageBlock, mode: .absolute) try engine.block.setHeightMode(imageBlock, mode: .absolute) try engine.block.setWidth(imageBlock, value: 520, maintainCrop: true) try engine.block.setHeight(imageBlock, value: 320, maintainCrop: true) let contentFillMode = try engine.block.getContentFillMode(imageBlock) print("Content fill mode is crop: \(contentFillMode == .crop)") ``` Use this for user-adjusted images or layout changes where visual continuity matters. Leave `maintainCrop` at its default when crop values should stay unadjusted and the visible framing can change according to the current fill and crop state. For reusable code that may receive other block types, call `engine.block.supportsContentFillMode(_:)` before reading or setting the content fill mode. ## Resizing Groups Group multiple blocks, then resize the group block to keep the members together: ```swift highlight-resizeImages-groupResize let group = try engine.block.group([imageBlock, secondImageBlock]) try engine.block.setWidth(group, value: 600) let groupWidth = try engine.block.getWidth(group) print("Group width: \(groupWidth)") ``` When a group is resized, CE.SDK keeps the group aspect ratio and updates both dimensions proportionally. ## Content-Aware Resizing Use `resizeContentAware(_:width:height:)` when changing page dimensions for another output format: ```swift highlight-resizeImages-contentAwareResize try engine.block.resizeContentAware([page], width: 1080, height: 1080) let pageWidth = try engine.block.getWidth(page) print("Page width after content-aware resize: \(pageWidth)") ``` This keeps full-page blocks attached to the page and scales other content proportionally. ## Locking Resize Operations Resize permissions have two layers: a **global scope** that sets the scene-wide default, and a **block-level scope** that overrides it. The block-level setting only takes effect when the global scope is `.defer`. Under the default Creator role every global scope is `.allow`, so block-level settings are ignored until you defer the global scope first. To keep a template block at its configured size, set the global `layer/resize` scope to `.defer`, then disable it on the specific block. Read the effective permission with `isAllowedByScope(_:key:)`, which evaluates both layers — `isScopeEnabled(_:key:)` only reports the block-level flag. For an unconditional lock that also prevents moving and rotating, use `setTransformLocked(_:locked:)`: ```swift highlight-resizeImages-lockResize try engine.editor.setGlobalScope(key: "layer/resize", value: .defer) try engine.block.setScopeEnabled(group, key: "layer/resize", enabled: false) let resizeAllowed = try engine.block.isAllowedByScope(group, key: "layer/resize") print("Resize allowed: \(resizeAllowed)") try engine.block.setTransformLocked(group, locked: true) let locked = try engine.block.isTransformLocked(group) print("Transform locked: \(locked)") ``` See [Lock Content](https://img.ly/docs/cesdk/mac-catalyst/rules/lock-content-9fa727/) for the full scope and permission model. ## Troubleshooting ### Image Not Resizing Check whether the `layer/resize` scope is disabled or the block is transform-locked. Then verify that the block exists and that the width and height modes match the values you write. ### Unexpected Size Values Read `getWidthMode(_:)` and `getHeightMode(_:)` before interpreting numeric values. In percentage mode, `0.5` means 50 percent of the parent, not 0.5 design units. ### Image Appears Cropped Resize changes the frame around the image content. Use `maintainCrop: true` to preserve the existing crop framing, or adjust the crop after resize when you want a different composition. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.create(_:)` | Create a page or graphic block | | `engine.block.createShape(_:)` | Create the shape used by a graphic block | | `engine.block.setShape(_:shape:)` | Attach a shape to a graphic block | | `engine.block.createFill(_:)` | Create an image fill | | `engine.block.setURL(_:property:value:)` | Set the image URI on the fill | | `engine.block.setFill(_:fill:)` | Attach a fill to a block | | `engine.block.appendChild(to:child:)` | Add the image block to the page | | `engine.block.setPositionX(_:value:)` | Set a block's x position | | `engine.block.setPositionY(_:value:)` | Set a block's y position | | `engine.editor.setResizeHandlesVisibility(_:)` | Set when editor resize handles are shown | | `engine.editor.getResizeHandlesVisibility()` | Read when editor resize handles are shown | | `engine.block.setWidthMode(_:mode:)` | Set how CE.SDK interprets the width value | | `engine.block.setHeightMode(_:mode:)` | Set how CE.SDK interprets the height value | | `engine.block.setWidth(_:value:maintainCrop:)` | Set a block width and optionally preserve crop state | | `engine.block.setHeight(_:value:maintainCrop:)` | Set a block height and optionally preserve crop state | | `engine.block.getWidth(_:)` | Read the configured width value | | `engine.block.getHeight(_:)` | Read the configured height value | | `engine.block.getWidthMode(_:)` | Read the width mode | | `engine.block.getHeightMode(_:)` | Read the height mode | | `engine.block.getFrameWidth(_:)` | Read the resolved frame width after layout | | `engine.block.getFrameHeight(_:)` | Read the resolved frame height after layout | | `engine.block.supportsContentFillMode(_:)` | Check whether a block exposes content fill mode | | `engine.block.setContentFillMode(_:mode:)` | Set how image content fills its frame | | `engine.block.getContentFillMode(_:)` | Read how image content fills its frame | | `engine.block.group(_:)` | Group blocks before resizing them together | | `engine.block.resizeContentAware(_:width:height:)` | Resize blocks while adjusting contained content | | `engine.editor.setGlobalScope(key:value:)` | Set the scene-wide default for a scope | | `engine.block.setScopeEnabled(_:key:enabled:)` | Enable or disable a scope on a block (applies when the global scope is `.defer`) | | `engine.block.isAllowedByScope(_:key:)` | Read the effective permission after evaluating global and block-level scopes | | `engine.block.setTransformLocked(_:locked:)` | Lock or unlock all transforms on a block | | `engine.block.isTransformLocked(_:)` | Read the transform lock state | ## Next Steps - Resize images proportionally with [Scale](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/scale-ebe367/). - Control image framing and visible content with [Crop](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/crop-f67a47/). - Apply resizing across complete designs with [Auto-Resize](https://img.ly/docs/cesdk/mac-catalyst/automation/auto-resize-4c2d58/). --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Rotate" description: "Documentation for Rotate" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/rotate-5f39c9/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/) > [Rotate](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/rotate-5f39c9/) --- Rotation is a common transform you apply to images to: - straighten horizons - add dynamic tilt - correct orientation issues Learn how to programmatically and interactively rotate images in your app using CE.SDK. Rotation applies at the block level, rotating the entire graphic on the canvas. This differs from [crop rotation](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/crop-f67a47/), which rotates the content inside the block frame. This guide focuses on block rotation and shows how to wire interactive controls into your SwiftUI app. ## What you'll learn - How to rotate an image as a user using the handles - Rotate an image block by a specific angle - How to lock image rotation - How to rotate multiple images as a group ## When You’ll Use It Use rotation when: - Straightening an image or aligning it with other design elements. - Adding expressive tilt to photos or stickers. - Correcting imported images that appear sideways. - Building custom editing experiences where users can freely or incrementally rotate media. ## Understanding Rotation in CE.SDK Rotation’s center is the block’s center point, and its definition is in radians. ### Block Rotation vs Crop Rotation - Block rotation moves the whole block on the canvas. - Crop rotation turns the content inside the crop region and is part of the crop API. Usage depends on your goal: - To **tilt the image visually** on the canvas, use block rotation. - To **rotate the content** inside a zoomed or constrained frame, use crop rotation. See the Crop guide: [Crop](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/crop-f67a47/). ### Content Fill Mode Rotation can change how your content fits inside its frame: for example, a rotated image using `.contain` may reveal empty areas that `.cover` would fill. You rarely need to update fill mode for rotation, but know that rotated images sometimes behave differently. ## Rotate an Image Using the UI By default selecting a block will show handles for resizing and rotating. You can freeform rotate a block by dragging the rotation handle. ![Rotation handle of the control gizmo](assets/rotation-handle.png) ## Rotate an Image Using Code You can rotate an image block using the `setRotation` function. It takes the `id` of the block and a rotation amount in radians. ```swift try engine.block.setRotation(star, radians: .pi / 4) ``` If you need to convert between radians and degrees, multiply the number in degrees by pi and divide by 180. ```swift let angleInRadians: Double = angleInDegrees * Double.pi / 180 let angleInDegrees: Double = angleInRadians * 180 / Double.pi ``` You can discover the current rotation of a block using the `getRotation` function. ```swift let rotationOfStar = try engine.block.getRotation(starID) ``` Reset the rotation at any time by setting the `radians` to `0`. ```swift try engine.block.setRotation(blockID, radians: 0) ``` You can rotate a block incrementally by reading it’s current value and then adjusting it and setting the new value. ```swift let delta = 0.26 let currentRotation = try engine.block.getRotation(imageID) try engine.block.setRotation(imageID, radians: currentRotation + delta) ``` ## Lock Rotation You can remove the rotation handle from the UI by changing the setting for the engine. This will affect *all* blocks. ```swift try engine.editor.setSettingBool("controlGizmo/showRotateHandles", value: false) ``` Though the handle is gone, the user can still use the two finger rotation gesture on a touch device. You can disable that gesture with the following setting. ```swift try engine.editor.setSettingEnum("touch/rotateAction", value: "None") ``` When you want to lock only certain blocks, you can toggle the transform lock property. This will apply to all transformations for the block. ```swift try engine.block.setTransformLocked(star, locked: true) ``` To lock just the rotation transform for a block, set its rotation scope to `false`. ```swift try engine.editor.setScopeEnabled(imageID, key: "layer/rotate", enabled: false) ``` Refer to the template constraints guide for more detailed examples. ## Rotate a Group of Images To rotate multiple elements together, first add them to a `group` and then rotate the group. ```swift let groupId = try engine.block.group([star, textBlock]) engine.block.setRotation(groupId, radians: pi / 2) ``` ## Update UI During User Interaction (Advanced) Users can tap an image to select it and rotate it with the gizmo handle or a gesture. To keep any UI in sync with these updates, you’ll need to subscribe to block update events using the CE.SDK’s `event` api. ```swift //Event subscription func watchForUpdates() { guard let engine, let imageID else { return } Task { for await events in engine.event.subscribe(to: [imageID]) { // Look for updates to this specific block guard events.contains(where: { $0.type == .updated && $0.block == imageID }) else { continue } // Read the updated rotation value from the engine and update the //view's rotation variable //this will fire on _all_ updates, not just rotation if let newValue = try? engine.block.getRotation(imageID) { rotation = newValue } } } } ``` The preceding code subscribes to updates from a single block. Whenever that block updates, it reads the block’s rotation value and updates a `rotation` variable. You would call a function like this one at the end of your setup code for the `View`. ## Troubleshooting |Symptom|Likely Cause|Solution| |----|----|----| |Rotation does nothing|Block has rotation scope disabled or incorrect block ID|Enable layer/rotate or unlock transform. Check block ID value. Ensure block has been appended to the page| | Image appears offset after rotation |Pivot point isn’t at image center|Make sure the pivot point is centered (default is center). | |Rotation resets unexpectedly|Setting crop rotation instead of block rotation|Use `setRotation`, not crop APIs| |Image shows empty areas after rotation|Content fill mode exposing background|Use .cover or adjust frame| | Rotation handle not visible|Gizmo settings disabled| Check that interactive UI controls are enabled in the settings. | ## Next Steps Explore a minimal but complete code sample on [GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-image-rotate). Continue shaping your transform workflow with these related guides: - Use [Resize](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/resize-407242/) to change a block's width and height independently. - Use [Scale](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/scale-ebe367/) to scale a block uniformly from its center. - [Flip](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/flip-035e9f/) mirrors content horizontally or vertically. - Learn how to subscribe to block updates and sync UI state using [Events](https://img.ly/docs/cesdk/mac-catalyst/concepts/events-353f97/). --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Scale" description: "Resize images uniformly in your app." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/scale-ebe367/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Images](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/) > [Scale](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/scale-ebe367/) --- Scaling lets users enlarge or shrink a block directly on the canvas. In CE.SDK, scaling is a transform property that applies uniformly to most block types. This guide shows how to scale images using CE.SDK in your app. You’ll learn how to scale image blocks proportionally, scale groups, and apply scaling constraints to protect template structure. The standard UI already supports pinch-to-zoom and on-screen scale handles. Scaling programmatically gives you finer control. This is ideal for automation, custom UI, or template-driven apps. When you want to scale the image **inside** the block and leave the block dimensions unchanged, you’ll use [crop scale](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/crop-f67a47/) instead. ## What You’ll Learn - Scale images programmatically using Swift. - Scale images proportionally or non-uniformly. - Scale grouped elements. - Enable or disable scaling via pinch gestures or gizmo handles. ## When to Use Use image scaling when your UI needs to: - Let users zoom artwork smoothly without cropping - Enforce a canonical image size in templates - Support controls like sliders instead of gestures - Scale multiple elements together (logos, product bundles, captions) ## Scaling Basics On iOS, you scale blocks using the **block API**. The main pieces you’ll use are: - `engine.block.scale(_ id: DesignBlockID, to: Float, anchorX: Float = 0, anchorY: Float = 0)` - `width` / `height` and their modes (`width/mode`, `height/mode`) - crop-related properties like `crop/scaleX`, `crop/scaleY`, and `crop/translationX` / `Y` Control the size with the following scale values: - `1.0`: represents the **original** size. - Larger than `1.0`: **increases** the size. - Smaller than `1.0`: **shrinks** the size. > **Note:** The examples below use image blocks, but this same approach works for shapes, text, stickers, and groups as long as you have their `DesignBlockID`. ## Scale an Image Uniformly Uniform scaling uses the `scale(_ id: DesignBlockID, to: Float)` function. A scale value of `1.0` is the original scale. Values larger than `1.0` increase the scale of the block and values lower than `1.0` scale the block smaller. A value of `2.0`, for example makes the block twice as large. This scales the image to 150% of its original size. Because the default anchor point is the top-left corner, the block grows outward from that corner. ```swift try engine.block.scale(imageBlock, to: 1.5) ``` ![Original image and scaled image](../mobile-assets/scale-example-1.png) By default, the anchor point for the image when scaling is the origin point on the top left. The scale function has two optional parameters to move the anchor point in the x and y direction. They can have values between `0.0` and `1.0` This scales the image to 150% of its original size. The origin anchor point is 0.5, 0.5 so the image expands from the center. ```swift try engine.block.scale(block, to: 1.5, anchorX: 0.5, anchorY: 0.5) ``` ![Original image placed over the scaled image, aligned on the center anchor point](../mobile-assets/scale-example-2.png) ## Scale Non-Uniformly To stretch or compress only one axis, thus distorting an image, use this combination: - The crop scale function - The width or height function How you decide to make the adjustment will have different results. Below are three examples of scaling the original image in the x direction only. ![Allowing the engine to scale the image as you adjust the width of the block](../mobile-assets/scale-example-3.png) ```swift try engine.block.setWidthMode(imageBlock, mode: .absolute) let newWidth: Float = try engine.block.getWidth(imageBlock) * 1.5 try engine.block.setWidth(imageBlock, value: newWidth) ``` The image continues respecting its fill mode (usually `.cover`), so the content scales automatically as the frame widens. ![Using crop scale for the horizontal axis and adjusting the width of the block](../mobile-assets/scale-example-4.png) ```swift try engine.block.setCropScaleX(imageBlock, scaleX: 1.50) try engine.block.setWidthMode(imageBlock, mode: .absolute) let newWidth: Float = try engine.block.getWidth(imageBlock) * 1.5 try engine.block.setWidth(imageBlock, value: newWidth) ``` This uses crop scale to scale the image in a single direction and then adjusts the block's width to match the change. The change in width does not take the crop into account and so distorts the image as it's scaling the scaled image. ![Using crop scale for the horizontal axis and using the maintainCrop property when changing the width](../mobile-assets/scale-example-5.png) ```swift try engine.block.setCropScaleX(imageBlock, scaleX: 1.50) try engine.block.setWidthMode(imageBlock, mode: .absolute) let newWidth: Float = try engine.block.getWidth(imageBlock) * 1.5 try engine.block.setWidth(imageBlock, value: newWidth, maintainCrop: true) ``` By setting the `maintainCrop` option to true, expanding the width of the image by the scale factor respects the crop scale and the image is less distorted. ## Scale Images with Built-In Gestures or Gizmos The CE.SDK UI supports these interactions automatically: ### Pinch to Zoom Enabled by default: ```swift try engine.editor.setSettingEnum("touch/pinchAction", value: "Scale") ``` Set it to `"None"` to turn off pinch scaling. For environments with keyboard and mouse a similar property exists: ```swift try engine.editor.setSettingBool("mouse/enableZoom", value: true) ``` ### Gizmo Scale Handles The UI can show corner handles for drag-scaling: ```swift try engine.editor.setSettingBool("controlGizmo/showScaleHandles", value: true) ``` This mirrors the behavior of native editors. > **Note:** Changing these settings affects how the CE.SDK interprets user input. It doesn’t prevent you from scaling blocks programmatically with `scale(_:to:)`. ## Scale Multiple Elements Together If you combine multiple blocks into a group, scaling the group scales every member: ```swift let groupId = try engine.block.group([imageBlock, textBlock]) try engine.block.scale(groupId, to: 0.75) ``` This scales the entire group to 75%. ## Lock Scaling When working with templates, you can lock a block from scaling by setting its scope. The [guide on locking](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) provides more information. ```swift try engine.block.setScopeEnabled(imageBlock, key: "layer/resize", enabled: false) ``` To prevent users from applying **any** transform to a block: ```swift try engine.block.setTransformLocked(imageBlock, locked: true) ``` ## Troubleshooting |Symptom|Likely Cause|Fix| |---|---|---| |“Property not found: transform/scale/x”|Using old spec property names that no longer exist.|Replace with `engine.block.scale(_, to:)` for uniform scale. See [Crop](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/crop-f67a47/) for more on how crop scale affects scaling results.| |Image changes size but looks oddly distorted|Combining crop and width changes in a surprising way.|Use a simpler pattern: either change width alone, or use a controlled crop/scaleX + width approach and test with sample images.| |Pinch does nothing on canvas|Pinch scaling disabled|Ensure "touch/pinchAction" is `"Scale"`, the default, and not overridden in settings.| |Scale handles don’t appear|Gizmo handles disabled in editor settings|Set `controlGizmo/showScaleHandles` to true.| |Image won’t scale at all|Block is transform-locked or scope-locked|Check `transformLocked` and any related scopes like "layer/resize". Unlock or re-enable scope if needed.| ## Next Steps Once you’re comfortable scaling images, explore the other transform tools: - [Resize](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/resize-407242/) for changing the size of a block’s frame. - [Crop](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/crop-f67a47/) for changing what part of the image is visible. - [Rotate](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/rotate-5f39c9/) for rotating images around an anchor. - [Flip](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/flip-035e9f/) to mirror images horizontally or vertically. - [Move](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/move-818dd9/) to reposition blocks on the canvas. Together, these guides give you a complete picture of how to position and transform images in CE.SDK on iOS, macOS, and Catalyst. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Add Captions" description: "Add synchronized captions to video scenes with CE.SDK's caption tracks, caption blocks, subtitle import, styling, and burned-in video export." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/add-captions-f67565/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Add Captions](https://img.ly/docs/cesdk/mac-catalyst/edit-video/add-captions-f67565/) --- ```swift file=@cesdk_swift_examples/engine-guides-captions/Captions.swift reference-only import Foundation import IMGLYEngine @MainActor func addCaptions(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.editor.setSettingBool("features/videoCaptionsEnabled", value: true) try engine.block.setDuration(page, duration: 20) let baseURL = try engine.guidesBaseURL let video = try engine.block.create(.graphic) try engine.block.setShape(video, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) try engine.block.setURL( videoFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) try engine.block.setFill(video, fill: videoFill) try engine.block.setDuration(video, duration: 20) let videoTrack = try engine.block.create(.track) try engine.block.appendChild(to: page, child: videoTrack) try engine.block.appendChild(to: videoTrack, child: video) try engine.block.fillParent(videoTrack) // Decode at least one video frame before exporting page snapshots so the // hero capture shows real footage rather than the default black poster. try await engine.block.forceLoadAVResource(videoFill) let captionTrack = try engine.block.create(.captionTrack) try engine.block.appendChild(to: page, child: captionTrack) let manageOffsetsAutomatically = false try engine.block.setBool( captionTrack, property: "track/automaticallyManageBlockOffsets", value: manageOffsetsAutomatically, ) let caption1 = try engine.block.create(.caption) try engine.block.setString(caption1, property: "caption/text", value: "Caption text 1") let caption2 = try engine.block.create(.caption) try engine.block.setString(caption2, property: "caption/text", value: "Caption text 2") try engine.block.appendChild(to: captionTrack, child: caption1) try engine.block.appendChild(to: captionTrack, child: caption2) try engine.block.setDuration(caption1, duration: 3) try engine.block.setDuration(caption2, duration: 5) try engine.block.setTimeOffset(caption1, offset: 0) try engine.block.setTimeOffset(caption2, offset: 3) // Captions can also be loaded from SRT or VTT files. The text and timing of // each caption are read from the file. Point the URL at your own subtitle // file; here we write a short SRT to a temporary file for demonstration. let srtContents = """ 1 00:00:08,000 --> 00:00:11,000 Imported from an SRT file 2 00:00:11,000 --> 00:00:14,000 with its own text and timing. """ let srtURL = FileManager.default.temporaryDirectory.appendingPathComponent("captions.srt") try srtContents.write(to: srtURL, atomically: true, encoding: .utf8) let captions = try await engine.block.createCaptionsFromURI(srtURL) for caption in captions { try engine.block.appendChild(to: captionTrack, child: caption) } // Position and size sync only with caption blocks under the same caption track, // so configure them once on a single caption. try engine.block.setPositionX(caption1, value: 0.05) try engine.block.setPositionXMode(caption1, mode: .percent) try engine.block.setPositionY(caption1, value: 0.8) try engine.block.setPositionYMode(caption1, mode: .percent) try engine.block.setHeight(caption1, value: 0.15) try engine.block.setHeightMode(caption1, mode: .percent) try engine.block.setWidth(caption1, value: 0.9) try engine.block.setWidthMode(caption1, mode: .percent) // Style properties also sync only with caption blocks under the same caption // track. Set text color and drop shadow with dedicated styling setters, then // use property-keyed setters for the background color and automatic font sizing. try engine.block.setTextColor(caption1, color: Color.rgba(r: 0.9, g: 0.9, b: 0.0, a: 1.0)) try engine.block.setDropShadowEnabled(caption1, enabled: true) try engine.block.setDropShadowColor(caption1, color: Color.rgba(r: 0.0, g: 0.0, b: 0.0, a: 0.8)) try engine.block.setBackgroundColorEnabled(caption1, enabled: true) try engine.block.setColor(caption1, property: "backgroundColor/color", color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 0.7)) try engine.block.setBool(caption1, property: "caption/automaticFontSizeEnabled", value: true) try engine.block.setFloat(caption1, property: "caption/minAutomaticFontSize", value: 24) try engine.block.setFloat(caption1, property: "caption/maxAutomaticFontSize", value: 72) try await engine.captureGuide(page, label: "hero") let fadeInAnimation = try engine.block.createAnimation(.fade) try engine.block.setDuration(fadeInAnimation, duration: 0.3) try engine.block.setInAnimation(caption1, animation: fadeInAnimation) let exportsDirectory = FileManager.default.temporaryDirectory // Exporting the page as MP4 burns the caption text into every rendered frame. let videoStream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in videoStream { switch event { case let .progress(renderedFrames, encodedFrames, totalFrames): print("Rendered", renderedFrames, "frames and encoded", encodedFrames, "frames out of", totalFrames) case let .finished(video: blob): try blob.write(to: exportsDirectory.appendingPathComponent("captions.mp4")) } } } ``` Add synchronized captions to video scenes with CE.SDK's caption tracks, caption blocks, subtitle import, styling properties, and burned-in video export. ![A video page with a yellow caption rendered over surf footage at the bottom of the frame.](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-captions) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Captions in CE.SDK use the same block hierarchy as other video content: a page contains a video track and a caption track, and the caption track contains caption blocks. Each caption block stores text, a time offset, a duration, and styling properties. This guide focuses on the Engine API. The example below builds a video page, overlays manually authored and SRT-imported captions, styles them, adds a fade-in animation, and exports the page as an MP4 with the captions burned into every frame. ## Creating a Video Scene Create a video scene, append a page, set the page dimensions, and enable the video captions feature before adding caption blocks. ```swift highlight-addCaptions-setupScene let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.editor.setSettingBool("features/videoCaptionsEnabled", value: true) ``` `engine.scene.createVideo()` returns the scene's `DesignBlockID`. The `features/videoCaptionsEnabled` setting controls whether captions render and respond to caption-specific APIs — set it once after creating the scene. ## Setting Page Duration The page duration defines the time range where captions can appear. This guide uses a 20-second page. ```swift highlight-addCaptions-setPageDuration try engine.block.setDuration(page, duration: 20) ``` ## Adding a Video Clip Create a graphic block with a video fill, give it the page duration, place it on a regular video track, and size the track to fill the page. This gives the caption track real video frames to overlay during preview and export. ```swift highlight-addCaptions-addVideo let video = try engine.block.create(.graphic) try engine.block.setShape(video, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) try engine.block.setURL( videoFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) try engine.block.setFill(video, fill: videoFill) try engine.block.setDuration(video, duration: 20) let videoTrack = try engine.block.create(.track) try engine.block.appendChild(to: page, child: videoTrack) try engine.block.appendChild(to: videoTrack, child: video) try engine.block.fillParent(videoTrack) ``` `createFill(.video)` returns the video fill block, and `setURL(_:property:value:)` with the `fill/video/fileURI` property points it at the source file. The `track` parent acts as the timeline container; `fillParent(_:)` makes the track match the page size so the video covers the full frame. ## Creating a Caption Track A caption track groups caption blocks under the page. Create it before adding manual or imported captions so every caption has the correct parent. ```swift highlight-addCaptions-createCaptionTrack let captionTrack = try engine.block.create(.captionTrack) try engine.block.appendChild(to: page, child: captionTrack) ``` Caption tracks can either keep each caption's explicit time offset or manage offsets automatically from child durations. Keep `track/automaticallyManageBlockOffsets` set to `false` when you import SRT or VTT captions, or whenever you want to set custom offsets yourself; set it to `true` when captions should play back sequentially without gaps. ```swift highlight-addCaptions-manageOffsets let manageOffsetsAutomatically = false try engine.block.setBool( captionTrack, property: "track/automaticallyManageBlockOffsets", value: manageOffsetsAutomatically, ) ``` ## Adding Captions ### Creating Caption Blocks Create caption blocks with `DesignBlockType.caption`, write their text with the `caption/text` property, and append them to the caption track. ```swift highlight-addCaptions-createCaptions let caption1 = try engine.block.create(.caption) try engine.block.setString(caption1, property: "caption/text", value: "Caption text 1") let caption2 = try engine.block.create(.caption) try engine.block.setString(caption2, property: "caption/text", value: "Caption text 2") try engine.block.appendChild(to: captionTrack, child: caption1) try engine.block.appendChild(to: captionTrack, child: caption2) ``` ### Importing Captions from Subtitle Files Use `createCaptionsFromURI(_:)` to parse SRT or VTT files. CE.SDK reads each cue's text and timing, returns the created caption block IDs, and you append them to the caption track. ```swift highlight-addCaptions-importCaptions // Captions can also be loaded from SRT or VTT files. The text and timing of // each caption are read from the file. Point the URL at your own subtitle // file; here we write a short SRT to a temporary file for demonstration. let srtContents = """ 1 00:00:08,000 --> 00:00:11,000 Imported from an SRT file 2 00:00:11,000 --> 00:00:14,000 with its own text and timing. """ let srtURL = FileManager.default.temporaryDirectory.appendingPathComponent("captions.srt") try srtContents.write(to: srtURL, atomically: true, encoding: .utf8) let captions = try await engine.block.createCaptionsFromURI(srtURL) for caption in captions { try engine.block.appendChild(to: captionTrack, child: caption) } ``` Imported captions are normal caption blocks, so you can style them with the same APIs as manually authored ones. Supported file formats are SRT and VTT. ## Modifying Captions ### Timing With manual offsets, set the duration and time offset on each caption block. Time values are in seconds, so the second caption below starts where the first one ends. ```swift highlight-addCaptions-setTiming try engine.block.setDuration(caption1, duration: 3) try engine.block.setDuration(caption2, duration: 5) try engine.block.setTimeOffset(caption1, offset: 0) try engine.block.setTimeOffset(caption2, offset: 3) ``` ### Position and Size Caption position and size are synchronized between caption blocks that share the same caption track, so set the shared layout on one caption per track. Use percentage modes so the caption box stays proportional when the output resolution changes. ```swift highlight-addCaptions-positionSize // Position and size sync only with caption blocks under the same caption track, // so configure them once on a single caption. try engine.block.setPositionX(caption1, value: 0.05) try engine.block.setPositionXMode(caption1, mode: .percent) try engine.block.setPositionY(caption1, value: 0.8) try engine.block.setPositionYMode(caption1, mode: .percent) try engine.block.setHeight(caption1, value: 0.15) try engine.block.setHeightMode(caption1, mode: .percent) try engine.block.setWidth(caption1, value: 0.9) try engine.block.setWidthMode(caption1, mode: .percent) ``` ### Styling Caption styling properties also synchronize between caption blocks under the same caption track. The sample changes text color and drop shadow with dedicated styling setters, then uses property-keyed setters for the background color and automatic font sizing. ```swift highlight-addCaptions-styleCaptions // Style properties also sync only with caption blocks under the same caption // track. Set text color and drop shadow with dedicated styling setters, then // use property-keyed setters for the background color and automatic font sizing. try engine.block.setTextColor(caption1, color: Color.rgba(r: 0.9, g: 0.9, b: 0.0, a: 1.0)) try engine.block.setDropShadowEnabled(caption1, enabled: true) try engine.block.setDropShadowColor(caption1, color: Color.rgba(r: 0.0, g: 0.0, b: 0.0, a: 0.8)) try engine.block.setBackgroundColorEnabled(caption1, enabled: true) try engine.block.setColor(caption1, property: "backgroundColor/color", color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 0.7)) try engine.block.setBool(caption1, property: "caption/automaticFontSizeEnabled", value: true) try engine.block.setFloat(caption1, property: "caption/minAutomaticFontSize", value: 24) try engine.block.setFloat(caption1, property: "caption/maxAutomaticFontSize", value: 72) ``` If your app registers a caption-preset asset source, you can query it with `engine.asset.findAssets(sourceID:query:)` and apply a returned asset with `engine.asset.applyToBlock(sourceID:assetResult:block:)`. The example uses direct block properties so the guide does not depend on a particular preset source being registered. ## Caption Animations Caption blocks support the same animation APIs as other animatable blocks. Create an animation, set its duration, and assign it as an in, loop, or out animation. Keep entry animations short — readability and timing matter more than motion for captions. ```swift highlight-addCaptions-addAnimation let fadeInAnimation = try engine.block.createAnimation(.fade) try engine.block.setDuration(fadeInAnimation, duration: 0.3) try engine.block.setInAnimation(caption1, animation: fadeInAnimation) ``` Replace `setInAnimation(_:animation:)` with `setLoopAnimation(_:animation:)` for a continuous effect, or `setOutAnimation(_:animation:)` for an exit transition. `setInAnimation(_:animation:)` changes one caption. Captions on one caption track share their animations: a caption you add to a track with `appendChild(to:child:)` or `insertChild(into:child:at:)` takes the animations of the captions already there, in place of any animations it had, and a caption you make longer with `setDuration(_:duration:)` takes their animation durations again. A caption whose `appearance/animation` scope is locked keeps its own animations. ## Exporting Videos with Captions Exporting the page as MP4 burns the captions into every rendered frame. The captions become part of the video and render on any platform without further configuration. ```swift highlight-addCaptions-exportVideo // Exporting the page as MP4 burns the caption text into every rendered frame. let videoStream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in videoStream { switch event { case let .progress(renderedFrames, encodedFrames, totalFrames): print("Rendered", renderedFrames, "frames and encoded", encodedFrames, "frames out of", totalFrames) case let .finished(video: blob): try blob.write(to: exportsDirectory.appendingPathComponent("captions.mp4")) } } ``` `exportVideo(_:mimeType:)` returns an `AsyncThrowingStream` that emits `.progress(...)` events while encoding and a final `.finished(video:)` event with the encoded MP4 bytes as a `Data` value. Changes made to the scene after export starts do not appear in the exported file — the engine freezes the scene state for the export run. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Captions are not visible | The caption is not under a caption track on the page | Check the hierarchy: page → caption track → caption block | | Caption appears at the wrong time | The time offset or duration is wrong, or automatic offset management is enabled when custom offsets are expected | Read back `getTimeOffset(_:)` and `getDuration(_:)`, then check `track/automaticallyManageBlockOffsets` on the caption track | | Subtitle import fails | The URI does not resolve to a valid SRT or VTT file | Confirm the URI is reachable and the file is well-formed | | Styling is not applied | The property key is not a caption or text property | Use caption properties such as `caption/text`, `caption/automaticFontSizeEnabled`, and text styling setters | ## API Reference | Method | Purpose | | --- | --- | | `engine.scene.createVideo()` | Create a video scene | | `engine.editor.setSettingBool("features/videoCaptionsEnabled", value: _)` | Enable caption editing features | | `engine.block.create(.page)` | Create the page that holds the video and caption tracks | | `engine.block.create(.graphic)` | Create a block for the video clip | | `engine.block.createShape(.rect)` | Create a rectangular shape for the video block | | `engine.block.setShape(_:shape:)` | Assign the shape to the video block | | `engine.block.createFill(.video)` | Create a video fill | | `engine.block.setURL(_:property:value:)` (`fill/video/fileURI`) | Set the video file URL | | `engine.block.setFill(_:fill:)` | Assign the video fill to the video block | | `engine.block.create(.track)` | Create a video track | | `engine.block.create(.captionTrack)` | Create a caption track | | `engine.block.setBool(_:property:value:)` (`track/automaticallyManageBlockOffsets`) | Enable or disable automatic caption offset management | | `engine.block.create(.caption)` | Create a caption block | | `engine.block.createCaptionsFromURI(_:)` | Import SRT or VTT captions | | `engine.block.appendChild(to:child:)` | Add tracks and blocks to the hierarchy | | `engine.block.fillParent(_:)` | Size a track to the page | | `engine.block.setString(_:property:value:)` (`caption/text`) | Set caption text | | `engine.block.setTimeOffset(_:offset:)` | Set when a caption appears | | `engine.block.setDuration(_:duration:)` | Set page, video, caption, or animation duration | | `engine.block.getTimeOffset(_:)` | Read when a caption appears | | `engine.block.getDuration(_:)` | Read a page, video, caption, or animation duration | | `engine.block.setPositionX(_:value:)` / `setPositionXMode(_:mode:)` | Set the caption box x position and mode | | `engine.block.setPositionY(_:value:)` / `setPositionYMode(_:mode:)` | Set the caption box y position and mode | | `engine.block.setWidth(_:value:maintainCrop:)` / `setWidthMode(_:mode:)` | Set the caption box width and mode | | `engine.block.setHeight(_:value:maintainCrop:)` / `setHeightMode(_:mode:)` | Set the caption box height and mode | | `engine.block.setTextColor(_:color:in:)` | Set caption text color | | `engine.block.setDropShadowEnabled(_:enabled:)` | Enable the caption drop shadow | | `engine.block.setDropShadowColor(_:color:)` | Set caption drop shadow color | | `engine.block.setBackgroundColorEnabled(_:enabled:)` | Enable the caption background | | `engine.block.setColor(_:property:color:)` | Set the caption background color through `backgroundColor/color` | | `engine.block.setBool(_:property:value:)` (`caption/automaticFontSizeEnabled`) | Enable automatic caption font sizing | | `engine.block.setFloat(_:property:value:)` (`caption/minAutomaticFontSize`, `caption/maxAutomaticFontSize`) | Set the automatic font size bounds | | `engine.asset.findAssets(sourceID:query:)` | Query registered caption preset assets | | `engine.asset.applyToBlock(sourceID:assetResult:block:)` | Apply a preset asset to an existing caption block | | `engine.block.createAnimation(_:)` | Create an animation block | | `engine.block.setInAnimation(_:animation:)` | Assign an entry animation | | `engine.block.setLoopAnimation(_:animation:)` | Assign a looping animation | | `engine.block.setOutAnimation(_:animation:)` | Assign an exit animation | | `engine.block.exportVideo(_:mimeType:)` | Export the page with burned-in captions | ## Next Steps - [Trim Video Clips](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) — Control which portion of media plays back - [Join and Arrange Clips](https://img.ly/docs/cesdk/mac-catalyst/edit-video/join-and-arrange-3bbc30/) — Combine multiple video clips into sequences and organize them on the timeline using tracks and time offsets. - [Video Timeline Overview](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) — Use the timeline editor to arrange and edit video clips, audio, and animations frame by frame. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Add Watermark" description: "Add text and image watermarks to videos with timeline duration, positioning, opacity, and visibility controls in Swift." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/add-watermark-762ce6/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Add Watermark](https://img.ly/docs/cesdk/mac-catalyst/edit-video/add-watermark-762ce6/) --- Add text and image watermarks to video content for copyright protection, branding, and content attribution using CE.SDK's time-aware block system. ![A watermarked video frame with a copyright text watermark in the bottom-left corner and a semi-transparent logo watermark in the top-right corner](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-video-add-watermark) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Video watermarks in CE.SDK are design blocks placed over the video page. A text watermark is a regular text block; an image watermark is a graphic block with an image fill. Both blocks carry a `duration` and `timeOffset`, so the same time-aware properties that drive the video timeline keep watermarks visible for as long as you want. This guide creates one text watermark and one image watermark, positions and styles them for a representative video page, and sets their timeline so they remain visible across the entire clip. ```swift file=@cesdk_swift_examples/engine-guides-create-video-add-watermark/AddWatermark.swift reference-only import Foundation import IMGLYEngine @MainActor func addWatermark(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) try await engine.scene.create(fromVideo: videoURL) guard let page = try engine.scene.getCurrentPage() else { fatalError("Expected create(fromVideo:) to create a page.") } let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) let videoDuration = try engine.block.getDuration(page) let textWatermark = try engine.block.create(.text) try engine.block.setWidthMode(textWatermark, mode: .auto) try engine.block.setHeightMode(textWatermark, mode: .auto) try engine.block.replaceText(textWatermark, text: "All rights reserved") let textPadding: Float = 20 try engine.block.setPositionX(textWatermark, value: textPadding) try engine.block.setPositionY(textWatermark, value: pageHeight - textPadding - 24) try engine.block.setTextFontSize(textWatermark, fontSize: 8) try engine.block.setTextColor(textWatermark, color: .rgba(r: 1, g: 1, b: 1, a: 1)) try engine.block.setTextHorizontalAlignment(textWatermark, alignment: .left) try engine.block.setOpacity(textWatermark, value: 0.7) try engine.block.setDropShadowEnabled(textWatermark, enabled: true) try engine.block.setDropShadowColor(textWatermark, color: .rgba(r: 0, g: 0, b: 0, a: 0.8)) try engine.block.setDropShadowOffsetX(textWatermark, offsetX: 2) try engine.block.setDropShadowOffsetY(textWatermark, offsetY: 2) try engine.block.setDropShadowBlurRadiusX(textWatermark, blurRadiusX: 4) try engine.block.setDropShadowBlurRadiusY(textWatermark, blurRadiusY: 4) try engine.block.setDuration(textWatermark, duration: videoDuration) try engine.block.setTimeOffset(textWatermark, offset: 0) try engine.block.appendChild(to: page, child: textWatermark) try await engine.captureGuide(page, label: "after-text-watermark") let logoWatermark = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(logoWatermark, shape: rectShape) let imageFill = try engine.block.createFill(.image) let logoURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: logoURL) try engine.block.setFill(logoWatermark, fill: imageFill) try engine.block.setContentFillMode(logoWatermark, mode: .contain) let logoSize: Float = 80 let logoPadding: Float = 20 try engine.block.setWidth(logoWatermark, value: logoSize) try engine.block.setHeight(logoWatermark, value: logoSize) try engine.block.setPositionX(logoWatermark, value: pageWidth - logoSize - logoPadding) try engine.block.setPositionY(logoWatermark, value: logoPadding) try engine.block.setOpacity(logoWatermark, value: 0.6) try engine.block.setBlendMode(logoWatermark, mode: .normal) try engine.block.setDuration(logoWatermark, duration: videoDuration) try engine.block.setTimeOffset(logoWatermark, offset: 0) try engine.block.appendChild(to: page, child: logoWatermark) // Demo scaffolding: advance the playhead to the middle of the clip so the // hero capture lands on a representative video frame. try engine.block.setPlaybackTime(page, time: videoDuration / 2) try await engine.captureGuide(page, label: "hero") } ``` ## Creating the Scene Start from a video scene and read the page dimensions and duration. The dimensions drive the placement math, and the duration is reused for each watermark block so it stays visible from the first frame to the last. ```swift highlight-addWatermark-createScene let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) try await engine.scene.create(fromVideo: videoURL) guard let page = try engine.scene.getCurrentPage() else { fatalError("Expected create(fromVideo:) to create a page.") } let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) let videoDuration = try engine.block.getDuration(page) ``` `scene.create(fromVideo:)` creates a scene whose current page is sized to the source video and whose duration matches the clip length. `getCurrentPage()` returns the page that was created for the scene. ## Creating a Text Watermark Text watermarks are text blocks. Create one, let it size itself to its content, and position it near the bottom-left corner with some padding from the page edges. ```swift highlight-addWatermark-createTextWatermark let textWatermark = try engine.block.create(.text) try engine.block.setWidthMode(textWatermark, mode: .auto) try engine.block.setHeightMode(textWatermark, mode: .auto) try engine.block.replaceText(textWatermark, text: "All rights reserved") let textPadding: Float = 20 try engine.block.setPositionX(textWatermark, value: textPadding) try engine.block.setPositionY(textWatermark, value: pageHeight - textPadding - 24) ``` `SizeMode.auto` keeps the block's frame tied to its rendered text, so only the position needs to be set explicitly. The example values are tuned for the sample video page — scale `textPadding`, the Y-offset, and the font size below in proportion to your own page dimensions. ## Styling Text Watermarks Style the text for readability across changing video frames. ```swift highlight-addWatermark-styleTextWatermark try engine.block.setTextFontSize(textWatermark, fontSize: 8) try engine.block.setTextColor(textWatermark, color: .rgba(r: 1, g: 1, b: 1, a: 1)) try engine.block.setTextHorizontalAlignment(textWatermark, alignment: .left) try engine.block.setOpacity(textWatermark, value: 0.7) ``` White text with 70% opacity stays visible without obscuring the underlying video. ## Adding Drop Shadow for Visibility Drop shadows help the text stay legible when the underlying video frame is bright or busy. ```swift highlight-addWatermark-textDropShadow try engine.block.setDropShadowEnabled(textWatermark, enabled: true) try engine.block.setDropShadowColor(textWatermark, color: .rgba(r: 0, g: 0, b: 0, a: 0.8)) try engine.block.setDropShadowOffsetX(textWatermark, offsetX: 2) try engine.block.setDropShadowOffsetY(textWatermark, offsetY: 2) try engine.block.setDropShadowBlurRadiusX(textWatermark, blurRadiusX: 4) try engine.block.setDropShadowBlurRadiusY(textWatermark, blurRadiusY: 4) ``` A black shadow at 80% alpha with small offsets and blur radii adds contrast without making the watermark dominate the frame. ## Setting Text Watermark Duration Match the block's duration to the page duration and start it at the beginning of the timeline. ```swift highlight-addWatermark-textTimeline try engine.block.setDuration(textWatermark, duration: videoDuration) try engine.block.setTimeOffset(textWatermark, offset: 0) try engine.block.appendChild(to: page, child: textWatermark) ``` `setDuration(_:duration:)` controls how long the block is active during playback. `setTimeOffset(_:offset:)` sets when in the timeline it first appears, and `appendChild(to:child:)` adds it above the video content on the page. ## Creating an Image Watermark Image watermarks are graphic blocks with a rectangular shape and an image fill. ```swift highlight-addWatermark-createImageWatermark let logoWatermark = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(logoWatermark, shape: rectShape) let imageFill = try engine.block.createFill(.image) let logoURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: logoURL) try engine.block.setFill(logoWatermark, fill: imageFill) try engine.block.setContentFillMode(logoWatermark, mode: .contain) ``` The image URL is assigned to the fill through the `fill/image/imageFileURI` property. `ContentFillMode.contain` keeps the logo inside its frame without cropping; switch to `.cover` if you would rather have the image fill the frame and crop excess. ## Positioning Image Watermarks Place the logo where it does not cover important video content. ```swift highlight-addWatermark-positionImageWatermark let logoSize: Float = 80 let logoPadding: Float = 20 try engine.block.setWidth(logoWatermark, value: logoSize) try engine.block.setHeight(logoWatermark, value: logoSize) try engine.block.setPositionX(logoWatermark, value: pageWidth - logoSize - logoPadding) try engine.block.setPositionY(logoWatermark, value: logoPadding) ``` The logo is sized to 80×80 design units and placed in the top-right corner with 20 units of padding. Subtract the logo size and padding from `pageWidth` to right-align the block. ## Configuring Opacity and Blend Mode Control how the logo composites with the underlying video frame. ```swift highlight-addWatermark-imageOpacityBlend try engine.block.setOpacity(logoWatermark, value: 0.6) try engine.block.setBlendMode(logoWatermark, mode: .normal) ``` 60% opacity keeps the logo visible while letting the video show through. `BlendMode.normal` displays the logo without additional compositing — pick `.multiply` or `.screen` if you want the logo's colors to interact with the underlying video. ## Setting Image Watermark Duration Image watermarks need the same timeline configuration as text watermarks. ```swift highlight-addWatermark-imageTimeline try engine.block.setDuration(logoWatermark, duration: videoDuration) try engine.block.setTimeOffset(logoWatermark, offset: 0) try engine.block.appendChild(to: page, child: logoWatermark) ``` Matching the page duration keeps the logo visible for the full video; a zero `timeOffset` starts it with the first frame. ## Watermark Positioning Strategies Choose positions based on the watermark purpose: - Bottom-right corner: common for copyright notices and unobtrusive branding. - Top-right corner: useful for logos that should stay visible above lower-third content. - Bottom-left corner: a good alternative for text when the opposite corner contains important content. - Center: strongest protection for drafts or previews, at the cost of obscuring the video. Calculate positions from `getWidth(_:)` and `getHeight(_:)` on the page so the same code works across video aspect ratios. ## Best Practices ### Visibility - Use drop shadows on text watermarks to keep them legible across changing video frames. - Keep opacity between 50-70% for visible but unobtrusive branding. - Test watermark placement against representative frames from the source video, not only the first frame. ### Time Management - Match watermark duration to the page duration for full-video coverage. - Use a zero `timeOffset` for watermarks that should appear from the start. - For time-based variations, create separate watermark blocks with different offsets and durations. ## API Reference | Method | Purpose | | --- | --- | | `engine.scene.create(fromVideo:)` | Create a video scene from a remote URL | | `engine.scene.getCurrentPage()` | Get the page created for the video scene | | `engine.block.getWidth(_:)` | Read the page width for placement math | | `engine.block.getHeight(_:)` | Read the page height for placement math | | `engine.block.getDuration(_:)` | Read the page or watermark duration in seconds | | `engine.block.create(.text)` | Create a text watermark block | | `engine.block.setWidthMode(_:mode:)` | Let a text block size itself to its content with `.auto` | | `engine.block.setHeightMode(_:mode:)` | Let a text block size itself to its content with `.auto` | | `engine.block.replaceText(_:text:)` | Set the text watermark content | | `engine.block.setTextFontSize(_:fontSize:)` | Set text size | | `engine.block.setTextColor(_:color:)` | Set text color | | `engine.block.setTextHorizontalAlignment(_:alignment:)` | Set paragraph alignment | | `engine.block.setDropShadowEnabled(_:enabled:)` | Enable or disable drop shadow | | `engine.block.setDropShadowColor(_:color:)` | Set shadow color and alpha | | `engine.block.setDropShadowOffsetX(_:offsetX:)` | Set horizontal shadow offset | | `engine.block.setDropShadowOffsetY(_:offsetY:)` | Set vertical shadow offset | | `engine.block.setDropShadowBlurRadiusX(_:blurRadiusX:)` | Set horizontal shadow blur | | `engine.block.setDropShadowBlurRadiusY(_:blurRadiusY:)` | Set vertical shadow blur | | `engine.block.create(.graphic)` | Create an image watermark block | | `engine.block.createShape(.rect)` | Create a rectangular shape for the graphic block | | `engine.block.setShape(_:shape:)` | Apply the rectangular shape to the graphic block | | `engine.block.createFill(.image)` | Create an image fill for a logo | | `engine.block.setString(_:property:value:)` | Set the logo image URL via the `fill/image/imageFileURI` property | | `engine.block.setFill(_:fill:)` | Apply the image fill to the graphic block | | `engine.block.setContentFillMode(_:mode:)` | Fit the logo inside its frame with `.contain` | | `engine.block.setWidth(_:value:)` | Set watermark width | | `engine.block.setHeight(_:value:)` | Set watermark height | | `engine.block.setPositionX(_:value:)` | Set horizontal position | | `engine.block.setPositionY(_:value:)` | Set vertical position | | `engine.block.setOpacity(_:value:)` | Set watermark transparency | | `engine.block.setBlendMode(_:mode:)` | Set image watermark blend mode | | `engine.block.setDuration(_:duration:)` | Set timeline duration in seconds | | `engine.block.setTimeOffset(_:offset:)` | Set timeline start time in seconds | | `engine.block.appendChild(to:child:)` | Add the watermark to the page | ## Next Steps - [Lock the Template](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) — Lock watermark elements in templates so adopters cannot modify them - [To MP4](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-mp4-c998a8/) — Export watermarked videos with configurable encoding options - [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) — Use the timeline editor to arrange video clips and overlays - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) — Apply fonts, colors, alignment, and other styling options to customize text appearance --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Annotation" description: "Add timed text, shapes, and highlights to video scenes with CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/annotation-e9cbad/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Annotation](https://img.ly/docs/cesdk/mac-catalyst/edit-video/annotation-e9cbad/) --- ```swift file=@cesdk_swift_examples/engine-guides-annotation/Annotation.swift reference-only import IMGLYEngine @MainActor func annotation(engine: Engine) async throws { // Resolve the sample video against the engine's base URL. In an app you would // point this at your own media URL. This line is example scaffolding, not part // of the annotation lesson, so it stays outside the highlighted snippets. let baseURL = try engine.guidesBaseURL let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) let page = try createAnnotationScene(engine: engine, videoURL: videoURL) let text = try addTextAnnotation(engine: engine, page: page) let highlight = try addShapeAnnotation(engine: engine, page: page) let annotations = [text, highlight] let sync = AnnotationTimelineSync(engine: engine, page: page) sync.refresh(annotations) try seekToAnnotation(engine: engine, page: page, annotation: highlight) _ = try setAnnotationPlayback(engine: engine, page: page, playing: true, looping: true) try updateAnnotationText(engine: engine, annotation: text, text: "Replay this part") try moveAnnotation(engine: engine, annotation: highlight, x: 780, y: 260) try updateAnnotationTiming(engine: engine, annotation: highlight, start: 13.0, duration: 3.0) try removeAnnotation(engine: engine, annotation: text) } @MainActor private func createAnnotationScene(engine: Engine, videoURL: URL) throws -> DesignBlockID { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 20) let video = try engine.block.create(.graphic) try engine.block.setShape(video, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(video, fill: videoFill) let videoTrack = try engine.block.create(.track) try engine.block.appendChild(to: page, child: videoTrack) try engine.block.appendChild(to: videoTrack, child: video) try engine.block.fillParent(videoTrack) return page } @MainActor private func addTextAnnotation(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let text = try engine.block.create(.text) try engine.block.replaceText(text, text: "Watch this part!") try engine.block.setTextFontSize(text, fontSize: 32) try engine.block.setWidthMode(text, mode: .auto) try engine.block.setHeightMode(text, mode: .auto) try engine.block.setPositionX(text, value: 160) try engine.block.setPositionY(text, value: 560) try engine.block.setTimeOffset(text, offset: 5) try engine.block.setDuration(text, duration: 5) try engine.block.appendChild(to: page, child: text) return text } @MainActor private func addShapeAnnotation(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let highlight = try engine.block.create(.graphic) try engine.block.setShape(highlight, shape: engine.block.createShape(.star)) try engine.block.setWidth(highlight, value: 140) try engine.block.setHeight(highlight, value: 140) try engine.block.setPositionX(highlight, value: 700) try engine.block.setPositionY(highlight, value: 240) let fill = try engine.block.createFill(.color) try engine.block.setFill(highlight, fill: fill) try engine.block.setFillSolidColor(highlight, r: 1, g: 0, b: 0, a: 1) try engine.block.setTimeOffset(highlight, offset: 12) try engine.block.setDuration(highlight, duration: 4) try engine.block.appendChild(to: page, child: highlight) return highlight } private struct AnnotationTimelineState { var currentTime: Double var activeAnnotation: DesignBlockID? } @MainActor private final class AnnotationTimelineSync { private let engine: Engine private let page: DesignBlockID private(set) var state = AnnotationTimelineState(currentTime: 0, activeAnnotation: nil) private var pollingTask: Task? init(engine: Engine, page: DesignBlockID) { self.engine = engine self.page = page } // Call this from UI code that owns a lifecycle. It polls at a modest interval // so the UI stays responsive. func start(_ annotations: [DesignBlockID]) { pollingTask?.cancel() pollingTask = Task { [weak self] in while !Task.isCancelled { self?.refresh(annotations) try? await Task.sleep(nanoseconds: 200_000_000) // ~5 Hz } } } func refresh(_ annotations: [DesignBlockID]) { let currentTime = (try? engine.block.getPlaybackTime(page)) ?? 0 let active = annotations.first { annotation in guard engine.block.isValid(annotation) else { return false } return (try? engine.block.isVisibleAtCurrentPlaybackTime(annotation)) == true } state = AnnotationTimelineState(currentTime: currentTime, activeAnnotation: active) } func stop() { pollingTask?.cancel() pollingTask = nil } } @MainActor private func seekToAnnotation(engine: Engine, page: DesignBlockID, annotation: DesignBlockID) throws { guard try engine.block.supportsPlaybackTime(page) else { return } let start = try engine.block.getTimeOffset(annotation) try engine.block.setPlaybackTime(page, time: start) } @MainActor private func setAnnotationPlayback( engine: Engine, page: DesignBlockID, playing: Bool, looping: Bool, ) throws -> (isPlaying: Bool, isLooping: Bool) { try engine.block.setPlaying(page, enabled: playing) let isPlaying = try engine.block.isPlaying(page) try engine.block.setLooping(page, looping: looping) let isLooping = try engine.block.isLooping(page) return (isPlaying, isLooping) } @MainActor private func updateAnnotationText(engine: Engine, annotation: DesignBlockID, text: String) throws { try engine.block.replaceText(annotation, text: text) } @MainActor private func moveAnnotation(engine: Engine, annotation: DesignBlockID, x: Float, y: Float) throws { try engine.block.setPositionX(annotation, value: x) try engine.block.setPositionY(annotation, value: y) } @MainActor private func updateAnnotationTiming( engine: Engine, annotation: DesignBlockID, start: Double, duration: Double, ) throws { try engine.block.setTimeOffset(annotation, offset: start) try engine.block.setDuration(annotation, duration: duration) } @MainActor private func removeAnnotation(engine: Engine, annotation: DesignBlockID) throws { try engine.block.destroy(annotation) } ``` Annotations are timed visual overlays such as text labels, shapes, highlights, stickers, or images. In CE.SDK they are ordinary blocks placed above video content and made visible for a specific timeline range. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-annotation) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. ## When to Use Annotations Use annotations for tutorials, sports analysis, education, product demos, and other video workflows where viewers should notice a specific moment. Use captions instead when the content is synchronized spoken text. Annotations use the same Block API as other visual content. Timing is controlled in seconds with `setTimeOffset(_:offset:)` and `setDuration(_:duration:)`, and playback sync reads the page's current playback time. ## Annotation Blocks and Timeline Placement For a standalone video scene, create a video page with explicit dimensions and duration, add the media to a track, and append annotation blocks to the page after the media. Page children added later render above earlier content. ```swift highlight-annotation-timelinePlacement @MainActor private func createAnnotationScene(engine: Engine, videoURL: URL) throws -> DesignBlockID { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 20) let video = try engine.block.create(.graphic) try engine.block.setShape(video, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(video, fill: videoFill) let videoTrack = try engine.block.create(.track) try engine.block.appendChild(to: page, child: videoTrack) try engine.block.appendChild(to: videoTrack, child: video) try engine.block.fillParent(videoTrack) return page } ``` ## Add a Text Annotation A text annotation is a `.text` block. Position it like any other text block, then set the timeline range before appending it to the page. ```swift highlight-annotation-textAnnotation @MainActor private func addTextAnnotation(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let text = try engine.block.create(.text) try engine.block.replaceText(text, text: "Watch this part!") try engine.block.setTextFontSize(text, fontSize: 32) try engine.block.setWidthMode(text, mode: .auto) try engine.block.setHeightMode(text, mode: .auto) try engine.block.setPositionX(text, value: 160) try engine.block.setPositionY(text, value: 560) try engine.block.setTimeOffset(text, offset: 5) try engine.block.setDuration(text, duration: 5) try engine.block.appendChild(to: page, child: text) return text } ``` The example starts at `5.0` seconds and lasts `5.0` seconds, so it is visible from 5s to 10s on the page timeline. ## Add a Shape Annotation A shape annotation uses a graphic block with a vector shape and fill. This example creates a red star that appears after the text annotation. ```swift highlight-annotation-shapeAnnotation @MainActor private func addShapeAnnotation(engine: Engine, page: DesignBlockID) throws -> DesignBlockID { let highlight = try engine.block.create(.graphic) try engine.block.setShape(highlight, shape: engine.block.createShape(.star)) try engine.block.setWidth(highlight, value: 140) try engine.block.setHeight(highlight, value: 140) try engine.block.setPositionX(highlight, value: 700) try engine.block.setPositionY(highlight, value: 240) let fill = try engine.block.createFill(.color) try engine.block.setFill(highlight, fill: fill) try engine.block.setFillSolidColor(highlight, r: 1, g: 0, b: 0, a: 1) try engine.block.setTimeOffset(highlight, offset: 12) try engine.block.setDuration(highlight, duration: 4) try engine.block.appendChild(to: page, child: highlight) return highlight } ``` Any visual block can serve as an annotation. Use text for labels, graphics for highlights, and image or sticker blocks for branded markers. ## Synchronize Annotation UI with Playback Custom UI can poll the page playback time and mark whichever annotation is visible at that moment. Keep the polling interval modest, for example 100-200 ms, so the UI stays responsive. ```swift highlight-annotation-playbackSync private struct AnnotationTimelineState { var currentTime: Double var activeAnnotation: DesignBlockID? } @MainActor private final class AnnotationTimelineSync { private let engine: Engine private let page: DesignBlockID private(set) var state = AnnotationTimelineState(currentTime: 0, activeAnnotation: nil) private var pollingTask: Task? init(engine: Engine, page: DesignBlockID) { self.engine = engine self.page = page } // Call this from UI code that owns a lifecycle. It polls at a modest interval // so the UI stays responsive. func start(_ annotations: [DesignBlockID]) { pollingTask?.cancel() pollingTask = Task { [weak self] in while !Task.isCancelled { self?.refresh(annotations) try? await Task.sleep(nanoseconds: 200_000_000) // ~5 Hz } } } func refresh(_ annotations: [DesignBlockID]) { let currentTime = (try? engine.block.getPlaybackTime(page)) ?? 0 let active = annotations.first { annotation in guard engine.block.isValid(annotation) else { return false } return (try? engine.block.isVisibleAtCurrentPlaybackTime(annotation)) == true } state = AnnotationTimelineState(currentTime: currentTime, activeAnnotation: active) } func stop() { pollingTask?.cancel() pollingTask = nil } } ``` `refresh(_:)` performs a single tick — read it once after seeking or on demand. `start(_:)` runs the polling loop until you call `stop()`; call it from UI code that owns the lifecycle. > **Note:** * Engine calls must stay on the main thread. `AnnotationTimelineSync` is annotated `@MainActor` so its polling task inherits the main actor. > * Store the active annotation ID in UI state and render your own list, marker, or toolbar state from that value. ## Seek to an Annotation Seek on the page, not on the annotation block itself. Read the annotation start with `getTimeOffset(_:)`, then set the page playback time. ```swift highlight-annotation-seek @MainActor private func seekToAnnotation(engine: Engine, page: DesignBlockID, annotation: DesignBlockID) throws { guard try engine.block.supportsPlaybackTime(page) else { return } let start = try engine.block.getTimeOffset(annotation) try engine.block.setPlaybackTime(page, time: start) } ``` ## Controlling Playback (Play/Pause, Loop) Use page playback controls as supporting APIs for preview UI. For broader audio and video playback controls, see [Control Audio and Video](https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/). ```swift highlight-annotation-playbackControls @MainActor private func setAnnotationPlayback( engine: Engine, page: DesignBlockID, playing: Bool, looping: Bool, ) throws -> (isPlaying: Bool, isLooping: Bool) { try engine.block.setPlaying(page, enabled: playing) let isPlaying = try engine.block.isPlaying(page) try engine.block.setLooping(page, looping: looping) let isLooping = try engine.block.isLooping(page) return (isPlaying, isLooping) } ``` ## Edit and Remove Annotations Text, position, timing, and deletion use the same block APIs after an annotation exists. Keep these operations focused so your UI can call them from list actions or inspector controls. ```swift highlight-annotation-editText @MainActor private func updateAnnotationText(engine: Engine, annotation: DesignBlockID, text: String) throws { try engine.block.replaceText(annotation, text: text) } ``` ```swift highlight-annotation-move @MainActor private func moveAnnotation(engine: Engine, annotation: DesignBlockID, x: Float, y: Float) throws { try engine.block.setPositionX(annotation, value: x) try engine.block.setPositionY(annotation, value: y) } ``` ```swift highlight-annotation-retime @MainActor private func updateAnnotationTiming( engine: Engine, annotation: DesignBlockID, start: Double, duration: Double, ) throws { try engine.block.setTimeOffset(annotation, offset: start) try engine.block.setDuration(annotation, duration: duration) } ``` ```swift highlight-annotation-remove @MainActor private func removeAnnotation(engine: Engine, annotation: DesignBlockID) throws { try engine.block.destroy(annotation) } ``` ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.createVideo()` | Create a video scene that supports timeline playback. | | `engine.block.create(_:)` | Create text, graphic, page, and track blocks. | | `engine.block.setWidth(_:value:)` / `engine.block.setHeight(_:value:)` | Size pages and visual annotation blocks. | | `engine.block.setWidthMode(_:mode:)` / `engine.block.setHeightMode(_:mode:)` | Auto-size text annotations to their content. | | `engine.block.replaceText(_:text:)` | Set or update text annotation content. | | `engine.block.setTextFontSize(_:fontSize:)` | Set text annotation size. | | `engine.block.createShape(_:)` / `engine.block.setShape(_:shape:)` | Create a shape annotation. | | `engine.block.createFill(_:)` / `engine.block.setFill(_:fill:)` / `engine.block.setFillSolidColor(_:r:g:b:a:)` | Style graphic annotations. | | `engine.block.setURL(_:property:value:)` | Attach video media to a video fill. | | `engine.block.setPositionX(_:value:)` / `engine.block.setPositionY(_:value:)` | Place annotations on the page. | | `engine.block.setTimeOffset(_:offset:)` / `engine.block.getTimeOffset(_:)` | Set or read the annotation start time in seconds. | | `engine.block.setDuration(_:duration:)` / `engine.block.getDuration(_:)` | Set or read the annotation duration in seconds. | | `engine.block.appendChild(to:child:)` | Add annotations to the page hierarchy. | | `engine.block.fillParent(_:)` | Make a track fill its parent page. | | `engine.block.isValid(_:)` | Ignore annotations that were removed before a UI refresh. | | `engine.block.supportsPlaybackTime(_:)` | Check whether a page can be seeked. | | `engine.block.setPlaybackTime(_:time:)` / `engine.block.getPlaybackTime(_:)` | Seek or read timeline playback time. | | `engine.block.isVisibleAtCurrentPlaybackTime(_:)` | Determine whether an annotation is active at the current page time. | | `engine.block.setPlaying(_:enabled:)` / `engine.block.isPlaying(_:)` | Start or query playback. | | `engine.block.setLooping(_:looping:)` / `engine.block.isLooping(_:)` | Control loop behavior. | | `engine.block.destroy(_:)` | Remove an annotation. | ### Properties | Property | Type | Description | | --- | --- | --- | | `fill/video/fileURI` | URL | Video media source for a video fill. | ## Troubleshooting - **Annotation does not show up:** append it to the page after the media track, and keep its `timeOffset` plus `duration` inside the page duration. - **Seek jumps do nothing:** call `setPlaybackTime(_:time:)` on the page block, not on the annotation block. - **UI feels sluggish:** poll at 5-10 Hz and keep engine calls on the main thread. - **Export differs from preview:** verify the video scene duration and test very small text or heavy effects in exported MP4 output. ## Next Steps - [Add Captions](https://img.ly/docs/cesdk/mac-catalyst/edit-video/add-captions-f67565/) - Use caption blocks and tracks for synchronized spoken text. - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) - Populate labels from dynamic values such as usernames or scores. - [Control Audio and Video](https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/) - Control timeline playback, trim ranges, looping, and resources. - [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) - Build timeline interfaces for arranging clips and overlays. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Force Trim" description: "Enforce minimum and maximum video durations in the editor UI." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/force-trim-3c1e8a/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Force Trim](https://img.ly/docs/cesdk/mac-catalyst/edit-video/force-trim-3c1e8a/) --- ```swift file=@cesdk_swift_examples/editor-guides-force-trim/ForceTrimSolution.swift reference-only import IMGLYEditor import SwiftUI struct ForceTrimSolution: View { let settings = EngineSettings( license: secrets.licenseKey, // pass nil for evaluation mode with watermark userID: "", ) var editor: some View { Editor(settings) .imgly.configuration { VideoEditorConfiguration { builder in builder.onLoaded { context, _ in context.setVideoDurationConstraints( minimumVideoDuration: 5, maximumVideoDuration: 15, ) } } } } @State private var isPresented = false var body: some View { Button("Use the Editor") { isPresented = true } .fullScreenCover(isPresented: $isPresented) { ModalEditor { editor } } } } #Preview { ForceTrimSolution() } ``` Force trim lets you enforce minimum and maximum video durations in the timeline UI. The editor clamps export to the maximum duration and shows labels to communicate the limits. ## Configure duration constraints Apply constraints in the `EditorConfiguration.onLoaded` callback after the scene has loaded. Use seconds for the values and ensure the max is not smaller than the min. ```swift highlight-forceTrim-constraints context.setVideoDurationConstraints( minimumVideoDuration: 5, maximumVideoDuration: 15, ) ``` ## Launch the video editor Present the `VideoEditor` as usual. You can call `setVideoDurationConstraints` again later to adjust limits at runtime. ```swift highlight-forceTrim-onLoaded builder.onLoaded { context, _ in context.setVideoDurationConstraints( minimumVideoDuration: 5, maximumVideoDuration: 15, ) } ``` ## Timeline and export behavior When the scene duration is below the minimum, the min label stays visible and the editor blocks export with a dialog. When the duration exceeds the maximum, the playhead sticks to the max position and export is clamped to that duration. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Join and Arrange Video Clips" description: "Combine multiple video clips into sequences and organize them on the timeline using tracks and time offsets in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/join-and-arrange-3bbc30/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Join and Arrange](https://img.ly/docs/cesdk/mac-catalyst/edit-video/join-and-arrange-3bbc30/) --- ```swift file=@cesdk_swift_examples/engine-guides-join-and-arrange-video/JoinAndArrangeVideo.swift reference-only import Foundation import IMGLYEngine @MainActor func joinAndArrangeVideo(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.setDuration(page, duration: 15) let baseURL = try engine.guidesBaseURL let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) let clipA = try await makeVideoClip(engine: engine, name: "Clip A", videoURL: videoURL, width: 1920, height: 1080) let clipB = try await makeVideoClip(engine: engine, name: "Clip B", videoURL: videoURL, width: 1920, height: 1080) let clipC = try await makeVideoClip(engine: engine, name: "Clip C", videoURL: videoURL, width: 1920, height: 1080) let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) try engine.block.setBool(track, property: "track/automaticallyManageBlockOffsets", value: false) try engine.block.appendChild(to: track, child: clipA) try engine.block.appendChild(to: track, child: clipB) try engine.block.appendChild(to: track, child: clipC) try engine.block.fillParent(track) let initialOrder = try engine.block.getChildren(track) assert(initialOrder == [clipA, clipB, clipC]) try engine.block.setDuration(clipA, duration: 5) try engine.block.setDuration(clipB, duration: 5) try engine.block.setDuration(clipC, duration: 5) try engine.block.setDuration(track, duration: 15) try engine.block.setTimeOffset(clipA, offset: 0) try engine.block.setTimeOffset(clipB, offset: 5) try engine.block.setTimeOffset(clipC, offset: 10) try engine.block.insertChild(into: track, child: clipC, at: 0) try engine.block.setTimeOffset(clipC, offset: 0) try engine.block.setTimeOffset(clipA, offset: 5) try engine.block.setTimeOffset(clipB, offset: 10) let children = try engine.block.getChildren(track) for (index, clip) in children.enumerated() { let name = try engine.block.getName(clip) let offset = try engine.block.getTimeOffset(clip) let duration = try engine.block.getDuration(clip) print("Position \(index): \(name) at \(offset)s for \(duration)s") } let finalNames = try children.map { try engine.block.getName($0) } let finalOffsets = try children.map { try engine.block.getTimeOffset($0) } assert(finalNames == ["Clip C", "Clip A", "Clip B"]) assert(finalOffsets == [0, 5, 10]) let overlayTrack = try engine.block.create(.track) try engine.block.appendChild(to: page, child: overlayTrack) try engine.block.setTimeOffset(overlayTrack, offset: 2) let overlayClip = try await makeVideoClip( engine: engine, name: "Overlay Clip", videoURL: videoURL, width: 1920 / 4, height: 1080 / 4, ) try engine.block.setDuration(overlayClip, duration: 5) try engine.block.appendChild(to: overlayTrack, child: overlayClip) try engine.block.setPositionX(overlayClip, value: 1920 - 1920 / 4 - 40) try engine.block.setPositionY(overlayClip, value: 1080 - 1080 / 4 - 40) } @MainActor private func makeVideoClip( engine: Engine, name: String, videoURL: URL, width: Float, height: Float, ) async throws -> DesignBlockID { let clip = try engine.block.create(.graphic) try engine.block.setName(clip, name: name) try engine.block.setShape(clip, shape: engine.block.createShape(.rect)) try engine.block.setWidth(clip, value: width) try engine.block.setHeight(clip, value: height) let videoFill = try engine.block.createFill(.video) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(clip, fill: videoFill) try engine.block.setContentFillMode(clip, mode: .cover) try await engine.block.forceLoadAVResource(videoFill) return clip } ``` Combine multiple video clips into a sequence and organize them in the composition using CE.SDK tracks, durations, and time offsets. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-join-and-arrange-video) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Video compositions in CE.SDK use a **Scene → Page → Track → Clip** hierarchy. Tracks group clips for timed playback. This guide sets clip durations and time offsets explicitly so each underlying API is visible; in most apps the default automatic mode covered in [Creating Tracks](https://img.ly/docs/cesdk/mac-catalyst/edit-video/join-and-arrange-3bbc30/#creating-tracks) handles sequencing for you. A clip is a graphic block with a video fill, so the same APIs that position, resize, or transform any other block apply to video. The example below builds a three-clip montage, reorders it, and adds an overlay track for a picture-in-picture composition. ## Joining Clips via UI CE.SDK's video editor includes timeline controls for arranging clips. On iOS, the [Video Editor Starter Kit](#broken-link-e1nlor) ships an interactive timeline UI you can start from. Use the Engine API sections below when you need to prepare scenes programmatically. ### Adding Clips to Timeline Users add clips from the asset library to the timeline. Adding a clip to an existing track joins it to that sequence; adding it to an empty area creates a separate track. The timeline displays clip duration visually, so longer clips take more horizontal space and the sequence is easy to scan. ### Reordering Clips Users can drag clips within a track to reorder them. The timeline updates the sequence and its time offsets so clips remain packed without gaps. ### Creating Additional Tracks Additional tracks create layered compositions. Tracks later in the page's child order render on top, which enables overlays, titles, and picture-in-picture layouts. ## Programmatic Clip Joining ### Creating the Scene Create a video scene, add a page, set a 16:9 frame, and make the page long enough for three 5-second clips. ```swift highlight-joinAndArrange-create-scene let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1920) try engine.block.setHeight(page, value: 1080) try engine.block.setDuration(page, duration: 15) ``` ### Creating Video Clips Each clip is built by composing a graphic block, a rectangle shape, and a video fill. The `makeVideoClip` helper defined in [Clip Helper](https://img.ly/docs/cesdk/mac-catalyst/edit-video/join-and-arrange-3bbc30/#clip-helper) below centralizes that construction and loads the video resource before returning the clip. ```swift highlight-joinAndArrange-create-clips let clipA = try await makeVideoClip(engine: engine, name: "Clip A", videoURL: videoURL, width: 1920, height: 1080) let clipB = try await makeVideoClip(engine: engine, name: "Clip B", videoURL: videoURL, width: 1920, height: 1080) let clipC = try await makeVideoClip(engine: engine, name: "Clip C", videoURL: videoURL, width: 1920, height: 1080) ``` ### Clip Helper ```swift highlight-joinAndArrange-clip-helper @MainActor private func makeVideoClip( engine: Engine, name: String, videoURL: URL, width: Float, height: Float, ) async throws -> DesignBlockID { let clip = try engine.block.create(.graphic) try engine.block.setName(clip, name: name) try engine.block.setShape(clip, shape: engine.block.createShape(.rect)) try engine.block.setWidth(clip, value: width) try engine.block.setHeight(clip, value: height) let videoFill = try engine.block.createFill(.video) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(clip, fill: videoFill) try engine.block.setContentFillMode(clip, mode: .cover) try await engine.block.forceLoadAVResource(videoFill) return clip } ``` `forceLoadAVResource(_:)` ensures the video metadata is available before downstream calls such as duration, thumbnails, or trim read from it. ### Creating Tracks Create a track and attach it to the page. The example also turns off automatic offset management on this track so it can demonstrate explicit `setTimeOffset(_:offset:)` calls in the next sections. In most apps, leaving the default automatic mode on is preferred — tracks then pack their children sequentially from the order they were appended. ```swift highlight-joinAndArrange-create-track let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) try engine.block.setBool(track, property: "track/automaticallyManageBlockOffsets", value: false) ``` ### Adding Clips to Track Append the clips to the track so they share a timeline container. `fillParent(_:)` on a track first resizes each child clip to fill the page frame, then resizes the track itself to match. Without it, the track inherits its dimensions from the children's intrinsic sizes — which happens to be 1920×1080 here, but calling `fillParent(_:)` keeps the track aligned with the page regardless of how individual clips were sized. The overlay track in [Multi-Track Compositions](https://img.ly/docs/cesdk/mac-catalyst/edit-video/join-and-arrange-3bbc30/#multi-track-compositions) skips this call because the overlay clip is intentionally smaller than the page. ```swift highlight-joinAndArrange-add-clips-to-track try engine.block.appendChild(to: track, child: clipA) try engine.block.appendChild(to: track, child: clipB) try engine.block.appendChild(to: track, child: clipC) try engine.block.fillParent(track) ``` ### Setting Clip Durations Set each clip duration in seconds. The track duration covers the full 15-second sequence. ```swift highlight-joinAndArrange-set-clip-durations try engine.block.setDuration(clipA, duration: 5) try engine.block.setDuration(clipB, duration: 5) try engine.block.setDuration(clipC, duration: 5) try engine.block.setDuration(track, duration: 15) ``` ## Arranging Clips ### Time Offsets Time offsets control when each block becomes active relative to its parent. In a manual sequence, set each child clip's offset to the cumulative duration of the preceding clips. ```swift highlight-joinAndArrange-time-offsets try engine.block.setTimeOffset(clipA, offset: 0) try engine.block.setTimeOffset(clipB, offset: 5) try engine.block.setTimeOffset(clipC, offset: 10) ``` Clip A starts at 0 seconds, Clip B at 5 seconds, and Clip C at 10 seconds. With 5-second durations, this creates a continuous 15-second sequence. ### Reordering Clips Use `insertChild(into:child:at:)` to move an existing clip to a specific index. The track's child order changes immediately. Update the time offsets after the move so playback follows the new order without gaps. ```swift highlight-joinAndArrange-reorder-clips try engine.block.insertChild(into: track, child: clipC, at: 0) try engine.block.setTimeOffset(clipC, offset: 0) try engine.block.setTimeOffset(clipA, offset: 5) try engine.block.setTimeOffset(clipB, offset: 10) ``` Moving Clip C to index 0 changes the order from A → B → C to C → A → B. The resulting offsets are C at 0 seconds, A at 5 seconds, and B at 10 seconds. ### Querying Track Children `getChildren(_:)` returns clip IDs in playback order. Combine it with `getName(_:)`, `getTimeOffset(_:)`, and `getDuration(_:)` to render a custom timeline view or to serialize the current arrangement to disk. ```swift highlight-joinAndArrange-query-track-children let children = try engine.block.getChildren(track) for (index, clip) in children.enumerated() { let name = try engine.block.getName(clip) let offset = try engine.block.getTimeOffset(clip) let duration = try engine.block.getDuration(clip) print("Position \(index): \(name) at \(offset)s for \(duration)s") } ``` ## Multi-Track Compositions ### Adding Multiple Tracks Create layered compositions by adding more tracks to the page. This example adds an overlay track that starts at 2 seconds and contains a smaller clip in the bottom-right corner. ```swift highlight-joinAndArrange-multi-track let overlayTrack = try engine.block.create(.track) try engine.block.appendChild(to: page, child: overlayTrack) try engine.block.setTimeOffset(overlayTrack, offset: 2) let overlayClip = try await makeVideoClip( engine: engine, name: "Overlay Clip", videoURL: videoURL, width: 1920 / 4, height: 1080 / 4, ) try engine.block.setDuration(overlayClip, duration: 5) try engine.block.appendChild(to: overlayTrack, child: overlayClip) try engine.block.setPositionX(overlayClip, value: 1920 - 1920 / 4 - 40) try engine.block.setPositionY(overlayClip, value: 1080 - 1080 / 4 - 40) ``` ### Track Rendering Order CE.SDK renders page children in order. The first track appears behind later tracks, so add background video first and overlays or titles later. - **Background layers**: Full-frame clips on the first track. - **Overlays**: Smaller clips positioned on later tracks. - **Titles**: Text or graphics added above the video tracks. ## Troubleshooting ### Clips Not Appearing Verify that every clip is attached to a track and that the track is attached to the page. `engine.block.getParent(_:)` and `engine.block.getChildren(_:)` are the quickest checks for hierarchy issues. ### Wrong Playback Order Check the track child order first. Default tracks automatically pack child offsets from that order and each clip's duration. When you manage timing manually, disable `track/automaticallyManageBlockOffsets` and write the offsets with `setTimeOffset(_:offset:)`. CE.SDK still prevents overlaps inside a single track; use separate tracks for overlapping or layered clips. ### Video Not Loading Check that the video URL is reachable and uses a supported format. Call `engine.block.forceLoadAVResource(_:)` on the video fill before you depend on media metadata such as duration or thumbnails. ## API Reference | Method | Description | | --- | --- | | `engine.scene.createVideo()` | Create a scene configured for video playback. | | `engine.block.create(.page)` | Create the page that holds the video composition. | | `engine.block.create(.track)` | Create a track for sequential or layered clips. | | `engine.block.create(.graphic)` | Create the graphic block used as a video clip. | | `engine.block.createFill(.video)` | Create the video fill attached to a clip block. | | `engine.block.appendChild(to:child:)` | Add a clip to a track or a track to a page hierarchy. | | `engine.block.insertChild(into:child:at:)` | Move or insert a child at a specific rendering-order index. | | `engine.block.getChildren(_:)` | Read child blocks in rendering order. | | `engine.block.setDuration(_:duration:)` | Set a page, track, or clip duration in seconds. | | `engine.block.getDuration(_:)` | Read a block's duration in seconds. | | `engine.block.setTimeOffset(_:offset:)` | Set when a block starts relative to its parent. | | `engine.block.getTimeOffset(_:)` | Read a block's time offset in seconds. | | `engine.block.setBool(_:property:value:)` | Set the `track/automaticallyManageBlockOffsets` flag to switch a track between automatic and manual child offset management. | | `engine.block.forceLoadAVResource(_:)` | Load audio or video metadata for a video fill or audio block. | | `engine.block.fillParent(_:)` | Resize and position a block to fill its parent frame; when the block is a group or track, child blocks are filled against the enclosing frame first. | ## Next Steps - [Trim Video Clips](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) — Control which portion of media plays back - [Control Audio and Video](https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/) — Master playback timing and audio mixing. - [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) — Understand the complete timeline editing system. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Programmatic Editing" description: "Edit video scenes with CE.SDK Engine APIs on Swift." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/programmatic-8429af/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Programmatic Editing](https://img.ly/docs/cesdk/mac-catalyst/edit-video/programmatic-8429af/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-video-edit-programmatic/EditVideoProgrammatically.swift reference-only import Foundation import IMGLYEngine @MainActor func editVideoProgrammatically(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 4.0) let baseURL = try engine.guidesBaseURL let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) try engine.block.fillParent(track) let firstClip = try engine.block.create(.graphic) try engine.block.setShape(firstClip, shape: engine.block.createShape(.rect)) let firstVideoFill = try engine.block.createFill(.video) try engine.block.setURL( firstVideoFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) try engine.block.setFill(firstClip, fill: firstVideoFill) try engine.block.setContentFillMode(firstClip, mode: .cover) let secondClipURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-kampus-production-8154913.mp4", ) let secondClip = try engine.block.create(.graphic) try engine.block.setShape(secondClip, shape: engine.block.createShape(.rect)) let secondVideoFill = try engine.block.createFill(.video) try engine.block.setURL(secondVideoFill, property: "fill/video/fileURI", value: secondClipURL) try engine.block.setFill(secondClip, fill: secondVideoFill) try engine.block.setContentFillMode(secondClip, mode: .cover) try engine.block.appendChild(to: track, child: firstClip) try engine.block.appendChild(to: track, child: secondClip) try await engine.block.forceLoadAVResource(firstVideoFill) try await engine.block.forceLoadAVResource(secondVideoFill) try engine.block.setDuration(firstClip, duration: 2.0) try engine.block.setDuration(secondClip, duration: 2.0) try engine.block.setTrimOffset(firstVideoFill, offset: 1.0) try engine.block.setTrimLength(firstVideoFill, length: 2.0) let secondSegment = try engine.block.split( secondClip, atTime: 1.0, options: SplitOptions(selectNewBlock: false), ) let overlay = try engine.block.create(.graphic) try engine.block.setShape(overlay, shape: engine.block.createShape(.rect)) let overlayFill = try engine.block.createFill(.color) try engine.block.setFill(overlay, fill: overlayFill) try engine.block.setColor( overlayFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.82, b: 0.1, a: 0.85), ) try engine.block.setWidth(overlay, value: 1280) try engine.block.setHeight(overlay, value: 72) try engine.block.setPositionY(overlay, value: 648) try engine.block.setTimeOffset(overlay, offset: 1.25) try engine.block.setDuration(overlay, duration: 1.5) try engine.block.appendChild(to: page, child: overlay) let mimeType: MIMEType = .mp4 let options = VideoExportOptions( videoBitrate: 8_000_000, audioBitrate: 128_000, framerate: 30, targetWidth: 1280, targetHeight: 720, ) let exportTask = Task { for try await export in try await engine.block.exportVideo(page, mimeType: mimeType, options: options) { switch export { case let .progress(renderedFrames, _, totalFrames): print("Rendered", renderedFrames, "of", totalFrames, "frames") case let .finished(video: videoData): return videoData } } return Blob() } let editedVideo = try await exportTask.value precondition(editedVideo.count > 0) // Keep references alive past their highlight blocks so SwiftLint doesn't // flag them — the rendered guide never shows these lines. _ = scene _ = secondSegment } ``` Edit video scenes with CE.SDK Engine APIs when your app needs automation, custom controls, or template-driven video output. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-video-edit-programmatic) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Programmatic editing works directly on the Engine scene graph rather than the built-in timeline UI for iOS. The [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) guide covers timeline-focused UI and Engine concepts. Use the programmatic approach when your app needs to create or modify video timelines from code, for example when generating variants, applying a known edit recipe, or exporting a scene without opening the editor. This guide builds a compact two-clip montage, trims the first clip, splits the second clip, adds a timed graphic overlay, and exports the edited page as MP4. ## Understand the Video Timeline A video scene contains one or more pages. A page owns tracks and timed blocks, and each track arranges its child clips in sequence. In a typical video timeline: - The scene is created in video mode with `engine.scene.createVideo()`. - A page defines the canvas size and the exported playback duration. - A track contains graphic clip blocks with video fills. - Audio blocks or overlay blocks can sit on the page timeline beside the track. - Audio-only media uses `DesignBlockType.audio`; video fills and audio blocks can both be muted or mixed with volume controls. Timeline values are measured in seconds. `setDuration(_:duration:)` controls how long a block is active, `setTimeOffset(_:offset:)` controls when a block starts within its parent timeline, and trim values control which source-media segment plays. ## Create a Video Scene Create a video scene, add a page, set the page size, and set the page duration. The page is the block exported later. ```swift highlight-editVideoProgrammatically-create-video-scene let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 4.0) ``` `createVideo()` enables timeline behavior. The page duration here is `4.0` seconds, so the export renders the first four seconds of the edited page. ## Add and Arrange Clips Create a track, attach it to the page, and append two graphic blocks with video fills. The track uses the child order to play clips sequentially. ```swift highlight-editVideoProgrammatically-add-clips let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) try engine.block.fillParent(track) let firstClip = try engine.block.create(.graphic) try engine.block.setShape(firstClip, shape: engine.block.createShape(.rect)) let firstVideoFill = try engine.block.createFill(.video) try engine.block.setURL( firstVideoFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) try engine.block.setFill(firstClip, fill: firstVideoFill) try engine.block.setContentFillMode(firstClip, mode: .cover) let secondClipURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-kampus-production-8154913.mp4", ) let secondClip = try engine.block.create(.graphic) try engine.block.setShape(secondClip, shape: engine.block.createShape(.rect)) let secondVideoFill = try engine.block.createFill(.video) try engine.block.setURL(secondVideoFill, property: "fill/video/fileURI", value: secondClipURL) try engine.block.setFill(secondClip, fill: secondVideoFill) try engine.block.setContentFillMode(secondClip, mode: .cover) try engine.block.appendChild(to: track, child: firstClip) try engine.block.appendChild(to: track, child: secondClip) ``` The sample sets `.cover` so each clip fills the page frame. Pick the mode that matches how the source video should fit into the graphic block: | Mode | Effect | | --- | --- | | `ContentFillMode.crop` | Uses manual crop positioning. | | `ContentFillMode.cover` | Scales content to cover the full block frame and can crop the edges. | | `ContentFillMode.contain` | Scales content to fit inside the block frame and can leave empty space. | ## Change Timing and Trim Load AV metadata before changing trim values or reading duration metadata. `forceLoadAVResource(_:)` is `async` and must be awaited. Then set each clip's timeline duration and trim the first fill to start one second into its source media. ```swift highlight-editVideoProgrammatically-change-timing-trim try await engine.block.forceLoadAVResource(firstVideoFill) try await engine.block.forceLoadAVResource(secondVideoFill) try engine.block.setDuration(firstClip, duration: 2.0) try engine.block.setDuration(secondClip, duration: 2.0) try engine.block.setTrimOffset(firstVideoFill, offset: 1.0) try engine.block.setTrimLength(firstVideoFill, length: 2.0) ``` Use these APIs for different timing layers: | API | Effect | | --- | --- | | `engine.block.setDuration(_:duration:)` | Sets how long the block is active on the timeline. | | `engine.block.setTimeOffset(_:offset:)` | Sets when the block starts within its parent timeline. | | `engine.block.setTrimOffset(_:offset:)` | Sets where source media playback starts. | | `engine.block.setTrimLength(_:length:)` | Sets how much source media is used for playback. | ## Split a Clip Split the second clip at one second. The original block becomes the first segment, and `split(_:atTime:options:)` returns the new second segment. ```swift highlight-editVideoProgrammatically-split-clip let secondSegment = try engine.block.split( secondClip, atTime: 1.0, options: SplitOptions(selectNewBlock: false), ) ``` The sample passes `SplitOptions(selectNewBlock: false)` because a headless workflow does not need to change editor selection after the split. `SplitOptions` controls how CE.SDK attaches and selects the new segment: | Field | Default | Effect | | --- | --- | --- | | `attachToParent` | `true` | Attaches the returned segment to the same parent as the original block. | | `createParentTrackIfNeeded` | `false` | Creates a parent track when the split block needs one and `attachToParent` is enabled. | | `selectNewBlock` | `true` | Selects the returned segment after splitting. | ## Apply a Timed Overlay Add a short graphic overlay directly to the page timeline. Its `timeOffset` and `duration` make it appear only for part of the exported video. ```swift highlight-editVideoProgrammatically-timed-overlay let overlay = try engine.block.create(.graphic) try engine.block.setShape(overlay, shape: engine.block.createShape(.rect)) let overlayFill = try engine.block.createFill(.color) try engine.block.setFill(overlay, fill: overlayFill) try engine.block.setColor( overlayFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.82, b: 0.1, a: 0.85), ) try engine.block.setWidth(overlay, value: 1280) try engine.block.setHeight(overlay, value: 72) try engine.block.setPositionY(overlay, value: 648) try engine.block.setTimeOffset(overlay, offset: 1.25) try engine.block.setDuration(overlay, duration: 1.5) try engine.block.appendChild(to: page, child: overlay) ``` This same pattern works for other programmatic edits: create or find the target block, set its timing, then apply the specific block or fill properties your workflow needs. ## Export the Edited Video Export the page as MP4 with `exportVideo(_:mimeType:options:)`. The call returns an `AsyncThrowingStream` that emits `.progress(...)` cases while rendering and a single `.finished(video:)` case with the encoded bytes. `VideoExportOptions` controls output size, frame rate, and bitrate. ```swift highlight-editVideoProgrammatically-export-video let mimeType: MIMEType = .mp4 let options = VideoExportOptions( videoBitrate: 8_000_000, audioBitrate: 128_000, framerate: 30, targetWidth: 1280, targetHeight: 720, ) let exportTask = Task { for try await export in try await engine.block.exportVideo(page, mimeType: mimeType, options: options) { switch export { case let .progress(renderedFrames, _, totalFrames): print("Rendered", renderedFrames, "of", totalFrames, "frames") case let .finished(video: videoData): return videoData } } return Blob() } let editedVideo = try await exportTask.value precondition(editedVideo.count > 0) ``` The sample asserts that the returned `Blob` is non-empty so the automated check verifies a real export result. `VideoExportOptions` exposes these public fields: | Field | Purpose | | --- | --- | | `h264Profile` | Selects the H.264 encoder profile. | | `h264Level` | Selects the H.264 encoder level — for example `52` for level 5.2. | | `videoBitrate` | Sets video bitrate in bits per second, or a named mode: `VideoBitrate.system` (`0`, default) lets the platform encoder choose, `VideoBitrate.auto` (`-1`) uses a bounded, resolution-aware bitrate. | | `audioBitrate` | Sets audio bitrate in bits per second, or `0` for automatic selection. | | `timeOffset` | Time offset in seconds into the scene timeline where the export starts. | | `duration` | Length of the exported video in seconds; `0` uses the page duration. | | `framerate` | Sets the target export frame rate in Hz. | | `targetWidth` | Sets the target output width when used with `targetHeight`. | | `targetHeight` | Sets the target output height when used with `targetWidth`. | | `allowTextOverhang` | Includes glyph overhang bounds to avoid clipping text during export. | ## API Reference | Swift API | Purpose | | --- | --- | | `engine.scene.createVideo()` | Create a scene in video mode. | | `engine.block.create(_:)` | Create pages, tracks, graphics, audio-only timeline blocks, and other blocks. | | `engine.block.createFill(_:)` | Create video or color fills. | | `engine.block.createShape(_:)` | Create a shape for a graphic block. | | `engine.block.appendChild(to:child:)` | Add pages, tracks, clips, and overlays to the hierarchy. | | `engine.block.setString(_:property:value:)` | Set the source URI on a video fill via `"fill/video/fileURI"`. | | `engine.block.setContentFillMode(_:mode:)` | Control how video content fits inside the graphic block. | | `engine.block.setDuration(_:duration:)` | Set page or block playback duration in seconds. | | `engine.block.setTimeOffset(_:offset:)` | Set when a block starts in its parent timeline. | | `engine.block.forceLoadAVResource(_:)` | Load audio or video metadata before trim and duration queries. | | `engine.block.getAVResourceTotalDuration(_:)` | Read the loaded media duration. | | `engine.block.setTrimOffset(_:offset:)` | Set the source media start offset. | | `engine.block.setTrimLength(_:length:)` | Set the source media playback length. | | `engine.block.split(_:atTime:options:)` | Split a timed block and return the second segment. | | `engine.block.setMuted(_:muted:)` | Mute audio on a video fill or audio block. | | `engine.block.setVolume(_:volume:)` | Set audio volume from `0` to `1` on a video fill or audio block. | | `engine.block.exportVideo(_:mimeType:options:)` | Export one edited page as a stream of progress events and final video bytes. | ## Next Steps - [Control Audio and Video](https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/) — Learn to play, pause, seek, and preview audio and video content in CE.SDK using playback controls and solo mode. - [Trim](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) — Control which portion of source media plays --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Redact Sensitive Content in Videos" description: "Redact sensitive video content using blur, pixelization, or solid overlays. Essential for privacy protection when obscuring faces, license plates, or personal information." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/redaction-cf6d03/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Redaction](https://img.ly/docs/cesdk/mac-catalyst/edit-video/redaction-cf6d03/) --- Redact sensitive video content using blur, pixelization, or solid overlays for privacy protection. ![Video redaction example showing a solid black overlay covering part of a surfing scene](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-redaction) CE.SDK applies effects to blocks themselves, not as overlays affecting content beneath. Redaction therefore means applying effects directly to the block you want to obscure. Four techniques cover most privacy scenarios: full-block blur, radial blur, pixelization, and solid overlays. ```swift file=@cesdk_swift_examples/engine-guides-redaction/Redaction.swift reference-only import IMGLYEngine // swiftlint:disable function_body_length @MainActor func redaction(engine: Engine) async throws { let segmentDuration = 5.0 let pageWidth: Float = 1280 let pageHeight: Float = 720 let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: pageWidth) try engine.block.setHeight(page, value: pageHeight) try engine.block.setDuration(page, duration: 5 * segmentDuration) let baseURL = try engine.guidesBaseURL let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) var videos: [DesignBlockID] = [] var videoFills: [DesignBlockID] = [] for index in 0 ..< 5 { let video = try engine.block.create(.graphic) try engine.block.setShape(video, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(video, fill: videoFill) try engine.block.appendChild(to: track, child: video) try engine.block.setDuration(video, duration: segmentDuration) try engine.block.setTimeOffset(video, offset: Double(index) * segmentDuration) videos.append(video) videoFills.append(videoFill) } try engine.block.fillParent(track) let radialVideo = videos[0] let fullBlurVideo = videos[1] let pixelVideo = videos[2] let timedVideo = videos[4] if try engine.block.supportsBlur(fullBlurVideo) { let uniformBlur = try engine.block.createBlur(.uniform) try engine.block.setFloat(uniformBlur, property: "blur/uniform/intensity", value: 0.7) try engine.block.setBlur(fullBlurVideo, blurID: uniformBlur) try engine.block.setBlurEnabled(fullBlurVideo, enabled: true) } if try engine.block.supportsEffects(pixelVideo) { let pixelizeEffect = try engine.block.createEffect(.pixelize) try engine.block.setInt(pixelizeEffect, property: "effect/pixelize/horizontalPixelSize", value: 24) try engine.block.setInt(pixelizeEffect, property: "effect/pixelize/verticalPixelSize", value: 24) try engine.block.appendEffect(pixelVideo, effectID: pixelizeEffect) try engine.block.setEffectEnabled(effectID: pixelizeEffect, enabled: true) } let overlay = try engine.block.create(.graphic) try engine.block.setShape(overlay, shape: engine.block.createShape(.rect)) let solidFill = try engine.block.createFill(.color) try engine.block.setColor( solidFill, property: "fill/color/value", color: .rgba(r: 0.1, g: 0.1, b: 0.1, a: 1.0), ) try engine.block.setFill(overlay, fill: solidFill) try engine.block.setWidth(overlay, value: pageWidth * 0.4) try engine.block.setHeight(overlay, value: pageHeight * 0.3) try engine.block.setPositionX(overlay, value: pageWidth * 0.55) try engine.block.setPositionY(overlay, value: pageHeight * 0.65) try engine.block.appendChild(to: page, child: overlay) // Show the overlay only during the fourth segment (15–20 seconds). try engine.block.setTimeOffset(overlay, offset: 3 * segmentDuration) try engine.block.setDuration(overlay, duration: segmentDuration) let timedBlur = try engine.block.createBlur(.uniform) try engine.block.setFloat(timedBlur, property: "blur/uniform/intensity", value: 0.9) try engine.block.setBlur(timedVideo, blurID: timedBlur) try engine.block.setBlurEnabled(timedVideo, enabled: true) let radialBlur = try engine.block.createBlur(.radial) try engine.block.setFloat(radialBlur, property: "blur/radial/blurRadius", value: 50) try engine.block.setFloat(radialBlur, property: "blur/radial/radius", value: 25) try engine.block.setFloat(radialBlur, property: "blur/radial/gradientRadius", value: 35) try engine.block.setFloat(radialBlur, property: "blur/radial/x", value: 0.5) try engine.block.setFloat(radialBlur, property: "blur/radial/y", value: 0.45) try engine.block.setBlur(radialVideo, blurID: radialBlur) try engine.block.setBlurEnabled(radialVideo, enabled: true) let sceneData = try await engine.scene.saveToString() _ = sceneData // Decode the video frames so the capture below renders the actual content // rather than a placeholder. for videoFill in videoFills { try await engine.block.forceLoadAVResource(videoFill) } // Hero: seek into the solid-overlay segment so the captured frame reads // unambiguously as a privacy redaction. try engine.block.setPlaybackTime(page, time: 17.5) try await engine.captureGuide(page, label: "hero") } // swiftlint:enable function_body_length ``` ## Creating the Scene Start with a scene and a single page sized to 16:9. The page duration must cover every segment you plan to redact — the example uses five 5-second segments, so the page runs for 25 seconds total. ```swift highlight-redaction-create-scene let pageWidth: Float = 1280 let pageHeight: Float = 720 let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: pageWidth) try engine.block.setHeight(page, value: pageHeight) try engine.block.setDuration(page, duration: 5 * segmentDuration) ``` ## Creating Video Blocks Each redaction technique is demonstrated on its own clip. A track holds the clips so they play back sequentially; assigning a `setTimeOffset` per clip places it at a specific point on the timeline. The example reuses the same video URL for every segment to keep the loop fast, but in your app each clip can use different content. ```swift highlight-redaction-create-videos let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) var videos: [DesignBlockID] = [] var videoFills: [DesignBlockID] = [] for index in 0 ..< 5 { let video = try engine.block.create(.graphic) try engine.block.setShape(video, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(video, fill: videoFill) try engine.block.appendChild(to: track, child: video) try engine.block.setDuration(video, duration: segmentDuration) try engine.block.setTimeOffset(video, offset: Double(index) * segmentDuration) videos.append(video) videoFills.append(videoFill) } try engine.block.fillParent(track) ``` `fillParent(track)` resizes the track to the page dimensions. The track's own layout then sizes each clip to fit the track. ## Understanding Redaction in CE.SDK ### How Effects Work Effects in CE.SDK modify the block's appearance directly rather than creating transparent overlays that affect content beneath. When you blur a video block, the entire block becomes blurred — not just a region on top of the video. ### Choosing a Redaction Technique Select the technique based on privacy requirements and visual impact: - **Full-block blur**: Complete obscuration for backgrounds or placeholder content - **Radial blur**: Circular blur patterns ideal for face-like regions - **Pixelization**: Clearly intentional censoring that renders faster than heavy blur - **Solid overlays**: Complete blocking for highly sensitive information like documents or credentials ## Programmatic Redaction ### Full-Block Blur When the entire video needs obscuring, apply blur directly to the original block. This approach works well for background content or privacy placeholders. ```swift highlight-redaction-full-block-blur if try engine.block.supportsBlur(fullBlurVideo) { let uniformBlur = try engine.block.createBlur(.uniform) try engine.block.setFloat(uniformBlur, property: "blur/uniform/intensity", value: 0.7) try engine.block.setBlur(fullBlurVideo, blurID: uniformBlur) try engine.block.setBlurEnabled(fullBlurVideo, enabled: true) } ``` Gate the work on `supportsBlur(_:)` so the code stays safe when applied to blocks that cannot accept blur. Create a uniform blur, configure its intensity with `blur/uniform/intensity` (a `Float` from `0.0` to `1.0`, where higher values produce stronger blur), attach it to the video block with `setBlur(_:blurID:)`, and enable it with `setBlurEnabled(_:enabled:)`. ### Pixelization Pixelization creates a mosaic effect that is clearly intentional and renders faster than heavy blur. It uses the effect system rather than the blur system. ```swift highlight-redaction-pixelization if try engine.block.supportsEffects(pixelVideo) { let pixelizeEffect = try engine.block.createEffect(.pixelize) try engine.block.setInt(pixelizeEffect, property: "effect/pixelize/horizontalPixelSize", value: 24) try engine.block.setInt(pixelizeEffect, property: "effect/pixelize/verticalPixelSize", value: 24) try engine.block.appendEffect(pixelVideo, effectID: pixelizeEffect) try engine.block.setEffectEnabled(effectID: pixelizeEffect, enabled: true) } ``` Check `supportsEffects(_:)` before creating the `pixelize` effect, then set `effect/pixelize/horizontalPixelSize` and `effect/pixelize/verticalPixelSize` to control the mosaic dimensions. Larger values produce stronger obscuration; values in the 15-30 range work well for standard redaction. ### Solid Overlays For complete blocking without any visual hint of the underlying content, create an opaque shape overlay. This approach does not require duplicating the video block. ```swift highlight-redaction-solid-overlay let overlay = try engine.block.create(.graphic) try engine.block.setShape(overlay, shape: engine.block.createShape(.rect)) let solidFill = try engine.block.createFill(.color) try engine.block.setColor( solidFill, property: "fill/color/value", color: .rgba(r: 0.1, g: 0.1, b: 0.1, a: 1.0), ) try engine.block.setFill(overlay, fill: solidFill) try engine.block.setWidth(overlay, value: pageWidth * 0.4) try engine.block.setHeight(overlay, value: pageHeight * 0.3) try engine.block.setPositionX(overlay, value: pageWidth * 0.55) try engine.block.setPositionY(overlay, value: pageHeight * 0.65) try engine.block.appendChild(to: page, child: overlay) // Show the overlay only during the fourth segment (15–20 seconds). try engine.block.setTimeOffset(overlay, offset: 3 * segmentDuration) try engine.block.setDuration(overlay, duration: segmentDuration) ``` Create a graphic with a rectangle shape and a solid color fill, then position and size it using absolute page coordinates. Use an alpha of `1.0` for complete opacity. The example attaches the overlay to the page (not the track) and gives it its own `setTimeOffset` and `setDuration` so it only appears during the fourth segment. ### Time-Based Redaction A redaction applied to a track clip is automatically time-bounded by the clip's own timeline window — the blur below covers the fifth segment because the clip itself only plays during 20–25 seconds. Use a stronger intensity (`0.9` here, versus `0.7` for the first blur) when a particular segment needs more aggressive obscuration. ```swift highlight-redaction-time-based-redaction let timedBlur = try engine.block.createBlur(.uniform) try engine.block.setFloat(timedBlur, property: "blur/uniform/intensity", value: 0.9) try engine.block.setBlur(timedVideo, blurID: timedBlur) try engine.block.setBlurEnabled(timedVideo, enabled: true) ``` For redactions that should appear or disappear *independent* of a clip — a censor bar that covers an always-visible region for only part of the timeline, a logo cover-up tied to a specific scene — give the redaction block its own `setTimeOffset(_:offset:)` and `setDuration(_:duration:)` instead, as shown in the [Solid Overlays](https://img.ly/docs/cesdk/mac-catalyst/edit-video/redaction-cf6d03/#solid-overlays) section above. ### Radial Blur For face-like regions, radial blur creates a circular blur pattern that fits rounded subjects. ```swift highlight-redaction-radial-blur let radialBlur = try engine.block.createBlur(.radial) try engine.block.setFloat(radialBlur, property: "blur/radial/blurRadius", value: 50) try engine.block.setFloat(radialBlur, property: "blur/radial/radius", value: 25) try engine.block.setFloat(radialBlur, property: "blur/radial/gradientRadius", value: 35) try engine.block.setFloat(radialBlur, property: "blur/radial/x", value: 0.5) try engine.block.setFloat(radialBlur, property: "blur/radial/y", value: 0.45) try engine.block.setBlur(radialVideo, blurID: radialBlur) try engine.block.setBlurEnabled(radialVideo, enabled: true) ``` Radial blur properties control the blur center (`blur/radial/x`, `blur/radial/y` from `0.0` to `1.0`), the unblurred center area (`blur/radial/radius`), the blur transition zone (`blur/radial/gradientRadius`), and the blur strength (`blur/radial/blurRadius`). ## Saving the Scene After applying redactions, serialize the scene to a string. Use this to persist the work for later editing or to hand off to an export pipeline. ```swift highlight-redaction-save-scene let sceneData = try await engine.scene.saveToString() ``` `saveToString()` returns the full scene definition, including all blocks, effects, and timeline data. Load it later with `engine.scene.load(from:)` to continue editing. ## Performance Considerations Different redaction techniques have different performance impacts: - **Solid overlays**: Minimal impact — you can create many without significant overhead. - **Pixelization**: Faster than blur; larger pixel sizes have minimal impact. - **Blur effects**: Higher intensity values increase rendering time, especially at high resolutions. For complex scenes with many redactions, prefer solid overlays where blur is not required, or reduce blur intensity to maintain smooth playback. ## Troubleshooting ### Redaction Not Visible If a redaction does not appear, verify that: - The overlay is a child of the page with `appendChild(to:child:)`. - Blur is enabled with `setBlurEnabled(_:enabled:)` after attaching it with `setBlur(_:blurID:)`. - Effects are enabled with `setEffectEnabled(effectID:enabled:)` after appending them with `appendEffect(_:effectID:)`. ### Performance Issues Reduce blur intensity, switch to pixelization instead of heavy blur, or use solid overlays for some redactions. ## Best Practices - **Preview thoroughly**: Scrub the entire timeline to verify all sensitive content is covered. - **Add safety margins**: Make redaction regions slightly larger than the sensitive area. - **Test at export resolution**: Higher resolutions may need stronger blur settings. - **Archive originals**: Exported redactions are permanent and cannot be reversed. - **Document redactions**: For compliance requirements, maintain records of what was redacted. ## API Reference | Method | Description | | ------ | ----------- | | `block.supportsBlur(_:)` | Check if a block supports blur effects | | `block.createBlur(_:)` | Create a blur instance (`.uniform`, `.radial`, `.linear`, `.mirrored`) | | `block.setBlur(_:blurID:)` | Attach a blur to a block | | `block.setBlurEnabled(_:enabled:)` | Enable or disable blur on a block | | `block.supportsEffects(_:)` | Check if a block supports effects | | `block.createEffect(_:)` | Create an effect instance (`.pixelize`, others) | | `block.appendEffect(_:effectID:)` | Add an effect to a block | | `block.setEffectEnabled(effectID:enabled:)` | Enable or disable an effect | | `block.setTimeOffset(_:offset:)` | Set when a block appears on the timeline | | `block.setDuration(_:duration:)` | Set how long a block remains on the timeline | | `block.create(_:)` | Create a block of the given type (`.graphic`, `.page`, `.track`, etc.) | | `block.createShape(_:)` | Create a shape (`.rect`) for graphic blocks | | `block.setShape(_:shape:)` | Assign a shape to a graphic block | | `block.createFill(_:)` | Create a fill (`.color`, `.video`, others) | | `block.setFill(_:fill:)` | Apply a fill to a block | | `block.setFloat(_:property:value:)` | Set a float property value | | `block.setInt(_:property:value:)` | Set an integer property value | | `block.setColor(_:property:color:)` | Set a color property value | | `scene.saveToString()` | Serialize the scene to a string | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Split Video and Audio" description: "Learn how to split video and audio clips at specific time points in CE.SDK for Swift, creating two independent segments from a single clip." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/split-464167/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Split](https://img.ly/docs/cesdk/mac-catalyst/edit-video/split-464167/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-video-split/Split.swift reference-only import Foundation import IMGLYEngine @MainActor func split(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 60) let baseURL = try engine.guidesBaseURL let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) func makeVideoBlock() async throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.video) try engine.block.setURL(fill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(block, fill: fill) try engine.block.appendChild(to: page, child: block) try await engine.block.forceLoadAVResource(fill) try engine.block.setDuration(block, duration: 10) return block } let basicVideo = try await makeVideoBlock() let newBlock = try engine.block.split(basicVideo, atTime: 5.0) _ = newBlock let optionsVideo = try await makeVideoBlock() let optionsNewBlock = try engine.block.split( optionsVideo, atTime: 4.0, options: SplitOptions( attachToParent: true, createParentTrackIfNeeded: false, selectNewBlock: false, ), ) _ = optionsNewBlock let playheadVideo = try await makeVideoBlock() // In a real app the user moves the playhead via the timeline UI; // here we position it programmatically so the demo has a known split point. try engine.block.setPlaybackTime(page, time: 3.0) let playheadTime = try engine.block.getPlaybackTime(page) let clipStartTime = try engine.block.getTimeOffset(playheadVideo) let splitTime = playheadTime - clipStartTime let playheadNewBlock = try engine.block.split(playheadVideo, atTime: splitTime) _ = playheadNewBlock let resultsVideo = try await makeVideoBlock() let resultsFill = try engine.block.getFill(resultsVideo) let originalTrimOffset = try engine.block.getTrimOffset(resultsFill) let originalTrimLength = try engine.block.getTrimLength(resultsFill) let resultsNewBlock = try engine.block.split(resultsVideo, atTime: 6.0) let resultsNewFill = try engine.block.getFill(resultsNewBlock) let originalAfterOffset = try engine.block.getTrimOffset(resultsFill) let originalAfterLength = try engine.block.getTrimLength(resultsFill) let newBlockTrimOffset = try engine.block.getTrimOffset(resultsNewFill) let newBlockTrimLength = try engine.block.getTrimLength(resultsNewFill) _ = (originalTrimOffset, originalTrimLength, originalAfterOffset, originalAfterLength) _ = (newBlockTrimOffset, newBlockTrimLength) let deleteVideo = try await makeVideoBlock() // Split at the start of the section to remove. let middleBlock = try engine.block.split(deleteVideo, atTime: 2.0) // Split again 3 seconds into middleBlock to mark the end of the section. let endBlock = try engine.block.split(middleBlock, atTime: 3.0) try engine.block.destroy(middleBlock) _ = endBlock let validateVideo = try await makeVideoBlock() try engine.block.setDuration(validateVideo, duration: 8.0) let blockDuration = try engine.block.getDuration(validateVideo) let desiredSplitTime = 4.0 var validatedNewBlock: DesignBlockID? if try engine.block.supportsTrim(validateVideo), desiredSplitTime > 0, desiredSplitTime < blockDuration { validatedNewBlock = try engine.block.split(validateVideo, atTime: desiredSplitTime) } _ = validatedNewBlock } ``` Split video and audio clips at specific time points using CE.SDK's Engine API for Swift to create independent segments from a single clip. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-video-split) Clip splitting divides one block into two at a specified time. The original block ends at the split point; a new block starts there. Both blocks reference the same source media with independent timing. This differs from trimming, which adjusts a single block's playback range without creating new blocks. This guide covers how to split clips programmatically using the Engine API, configure split options, calculate split positions from the playhead, and implement a split-and-delete workflow for removing middle sections. ## Setting Up the Scene Create a video scene, add a page, and give the page a fixed size and total duration so the split demos below have a stable timeline. ```swift highlight-setupScene let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 60) ``` ## Creating a Video Block Each section below splits a fresh 10-second video block. The `makeVideoBlock` helper creates a `.graphic` block with a rectangular shape and a video fill, appends it to the page, and assigns a 10-second timeline duration. The helper awaits `forceLoadAVResource(_:)` before returning—loading the media is mandatory because trim properties and `split(_:atTime:)` rely on the underlying clip's duration metadata. ```swift highlight-makeVideoBlock let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) func makeVideoBlock() async throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.video) try engine.block.setURL(fill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(block, fill: fill) try engine.block.appendChild(to: page, child: block) try await engine.block.forceLoadAVResource(fill) try engine.block.setDuration(block, duration: 10) return block } ``` ## Programmatic Splitting For applications that need to split clips programmatically—whether for automation, batch processing, or dynamic editing—CE.SDK provides the `engine.block.split(_:atTime:options:)` method. ### Basic Splitting at a Specific Time Split a block by providing the block ID and the split time in seconds. The time parameter is relative to the block's own timeline, accounting for the block's time offset. ```swift highlight-basic-split let basicVideo = try await makeVideoBlock() let newBlock = try engine.block.split(basicVideo, atTime: 5.0) ``` The `split(_:atTime:)` method returns the ID of the newly created block. The original block becomes the first segment (before the split point), and the returned block is the second segment (after the split point). ### Configuring Split Options The `SplitOptions` struct controls split behavior with three optional parameters: - **`attachToParent`** (default: `true`): Whether to attach the new block to the same parent as the original. - **`createParentTrackIfNeeded`** (default: `false`): Creates a parent track if needed and adds both blocks to it. Only applied when `attachToParent` is `true`. - **`selectNewBlock`** (default: `true`): Whether to select the newly created block after splitting. ```swift highlight-split-options let optionsVideo = try await makeVideoBlock() let optionsNewBlock = try engine.block.split( optionsVideo, atTime: 4.0, options: SplitOptions( attachToParent: true, createParentTrackIfNeeded: false, selectNewBlock: false, ), ) ``` Use `selectNewBlock: false` when splitting multiple clips programmatically to avoid changing selection state between operations. ### Splitting at the Current Playhead Position To implement playhead-based splitting like the built-in UI, get the current playback time from the page and convert it to block-relative time. ```swift highlight-split-at-playhead let playheadVideo = try await makeVideoBlock() // In a real app the user moves the playhead via the timeline UI; // here we position it programmatically so the demo has a known split point. try engine.block.setPlaybackTime(page, time: 3.0) let playheadTime = try engine.block.getPlaybackTime(page) let clipStartTime = try engine.block.getTimeOffset(playheadVideo) let splitTime = playheadTime - clipStartTime let playheadNewBlock = try engine.block.split(playheadVideo, atTime: splitTime) ``` The playhead position from `getPlaybackTime(_:)` on the page is in absolute timeline seconds. Subtract the clip's `getTimeOffset(_:)` to convert to block-relative time before passing it to `split(_:atTime:)`. ## Understanding Split Results After a split operation, both the original and new blocks are configured with updated trim properties. ### Trim Properties After Split Trim values are queried on the block's **fill**, not on the block itself. Use `getFill(_:)` to obtain the fill ID, then call `getTrimOffset(_:)` and `getTrimLength(_:)`. ```swift highlight-split-results let resultsVideo = try await makeVideoBlock() let resultsFill = try engine.block.getFill(resultsVideo) let originalTrimOffset = try engine.block.getTrimOffset(resultsFill) let originalTrimLength = try engine.block.getTrimLength(resultsFill) let resultsNewBlock = try engine.block.split(resultsVideo, atTime: 6.0) let resultsNewFill = try engine.block.getFill(resultsNewBlock) let originalAfterOffset = try engine.block.getTrimOffset(resultsFill) let originalAfterLength = try engine.block.getTrimLength(resultsFill) let newBlockTrimOffset = try engine.block.getTrimOffset(resultsNewFill) let newBlockTrimLength = try engine.block.getTrimLength(resultsNewFill) ``` The original block keeps its trim offset unchanged, but its trim length is reduced to the split point. The new block has its trim offset advanced by the split time and trim length set to cover the remaining duration. Both blocks reference the same source media—splitting is non-destructive. ### Timeline Positioning The original block keeps its `getTimeOffset(_:)`. When `attachToParent` is `true`, the new block is positioned immediately after the original on the same parent. Both blocks stay on the same track unless `createParentTrackIfNeeded` creates a new track structure. ## Split and Delete Workflow Remove a middle section from a clip by splitting at both boundaries and destroying the middle segment. ```swift highlight-split-and-delete let deleteVideo = try await makeVideoBlock() // Split at the start of the section to remove. let middleBlock = try engine.block.split(deleteVideo, atTime: 2.0) // Split again 3 seconds into middleBlock to mark the end of the section. let endBlock = try engine.block.split(middleBlock, atTime: 3.0) try engine.block.destroy(middleBlock) ``` This workflow is useful for removing unwanted sections, such as cutting out pauses, mistakes, or irrelevant portions from a recording. ## Validating Split Time Always validate that the split time is within valid bounds before calling `split(_:atTime:)`. The split time must be greater than `0` and less than the block's duration. Confirm the block supports trim with `supportsTrim(_:)` and read the available range with `getDuration(_:)`. ```swift highlight-validate-split-time let validateVideo = try await makeVideoBlock() try engine.block.setDuration(validateVideo, duration: 8.0) let blockDuration = try engine.block.getDuration(validateVideo) let desiredSplitTime = 4.0 var validatedNewBlock: DesignBlockID? if try engine.block.supportsTrim(validateVideo), desiredSplitTime > 0, desiredSplitTime < blockDuration { validatedNewBlock = try engine.block.split(validateVideo, atTime: desiredSplitTime) } ``` Attempting to split at an invalid time (at the beginning, end, or outside the block's duration) will fail or produce unexpected results. ## Troubleshooting ### Split Returns Unexpected Block If the returned block ID doesn't behave as expected, remember that the original block becomes the first segment (before split point) and the returned block is the second segment (after split point). ### Split Time Out of Range If split fails or produces unexpected results, verify the split time is within bounds. Use `getDuration(_:)` to check the valid range before splitting. ### Clip Not Splitting If `split(_:atTime:)` has no visible effect, check that the block type supports splitting. Verify `supportsTrim(_:)` returns `true` for the block. For video and audio fills, ensure `forceLoadAVResource(_:)` has been awaited before attempting to split. ## API Reference | Method | Description | Parameters | Returns | | ----------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------- | ---------------- | | `split(_:atTime:options:)` | Split a block at the specified time | `id: DesignBlockID, atTime: Double, options: SplitOptions` | `DesignBlockID` | | `SplitOptions(attachToParent:createParentTrackIfNeeded:selectNewBlock:)` | Configure split behavior | `attachToParent: Bool, createParentTrackIfNeeded: Bool, selectNewBlock: Bool` | `SplitOptions` | | `getTimeOffset(_:)` | Get time offset relative to parent | `id: DesignBlockID` | `Double` | | `getDuration(_:)` | Get playback duration | `id: DesignBlockID` | `Double` | | `getPlaybackTime(_:)` | Get current playback time | `id: DesignBlockID` | `Double` | | `getTrimOffset(_:)` | Get trim offset of the block's fill | `id: DesignBlockID` | `Double` | | `getTrimLength(_:)` | Get trim length of the block's fill | `id: DesignBlockID` | `Double` | | `supportsTrim(_:)` | Check if a block supports trim properties | `id: DesignBlockID` | `Bool` | | `forceLoadAVResource(_:)` | Force load media resource metadata | `id: DesignBlockID` | `Void` (async) | | `destroy(_:)` | Destroy a block | `id: DesignBlockID` | `Void` | ## Next Steps - [Trim Video and Audio](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) — Control playback range without splitting - [Video Timeline Overview](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) — Understand the complete timeline editing system --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Transform" description: "Learn how CE.SDK video transforms use block geometry, crop transforms, groups, animations, and transform permissions." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform-369f28/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform-369f28/) --- ```swift file=@cesdk_swift_examples/engine-guides-video-transform/VideoTransform.swift reference-only import Foundation import IMGLYEngine @MainActor func transformVideo(engine: Engine) throws { // Resolve the sample video against the engine's base URL. let baseURL = try engine.guidesBaseURL let sampleVideoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 8) // Four video blocks, one per transformation demonstrated below. let positionedVideo = try engine.block.create(.graphic) let positionedVideoFill = try engine.block.createFill(.video) try engine.block.setName(positionedVideo, name: "Positioned video") try engine.block.setShape(positionedVideo, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(positionedVideo, value: 80) try engine.block.setPositionY(positionedVideo, value: 80) try engine.block.setWidth(positionedVideo, value: 300) try engine.block.setHeight(positionedVideo, value: 170) try engine.block.setURL(positionedVideoFill, property: "fill/video/fileURI", value: sampleVideoURL) try engine.block.setFill(positionedVideo, fill: positionedVideoFill) try engine.block.setContentFillMode(positionedVideo, mode: .cover) try engine.block.setDuration(positionedVideo, duration: 8) try engine.block.appendChild(to: page, child: positionedVideo) let rotatedVideo = try engine.block.create(.graphic) let rotatedVideoFill = try engine.block.createFill(.video) try engine.block.setName(rotatedVideo, name: "Rotated video") try engine.block.setShape(rotatedVideo, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(rotatedVideo, value: 460) try engine.block.setPositionY(rotatedVideo, value: 80) try engine.block.setWidth(rotatedVideo, value: 300) try engine.block.setHeight(rotatedVideo, value: 170) try engine.block.setURL(rotatedVideoFill, property: "fill/video/fileURI", value: sampleVideoURL) try engine.block.setFill(rotatedVideo, fill: rotatedVideoFill) try engine.block.setContentFillMode(rotatedVideo, mode: .cover) try engine.block.setDuration(rotatedVideo, duration: 8) try engine.block.appendChild(to: page, child: rotatedVideo) let croppedVideo = try engine.block.create(.graphic) let croppedVideoFill = try engine.block.createFill(.video) try engine.block.setName(croppedVideo, name: "Cropped video") try engine.block.setShape(croppedVideo, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(croppedVideo, value: 80) try engine.block.setPositionY(croppedVideo, value: 360) try engine.block.setWidth(croppedVideo, value: 300) try engine.block.setHeight(croppedVideo, value: 170) try engine.block.setURL(croppedVideoFill, property: "fill/video/fileURI", value: sampleVideoURL) try engine.block.setFill(croppedVideo, fill: croppedVideoFill) try engine.block.setContentFillMode(croppedVideo, mode: .cover) try engine.block.setDuration(croppedVideo, duration: 8) try engine.block.appendChild(to: page, child: croppedVideo) let lockedVideo = try engine.block.create(.graphic) let lockedVideoFill = try engine.block.createFill(.video) try engine.block.setName(lockedVideo, name: "Locked video") try engine.block.setShape(lockedVideo, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(lockedVideo, value: 460) try engine.block.setPositionY(lockedVideo, value: 360) try engine.block.setWidth(lockedVideo, value: 300) try engine.block.setHeight(lockedVideo, value: 170) try engine.block.setURL(lockedVideoFill, property: "fill/video/fileURI", value: sampleVideoURL) try engine.block.setFill(lockedVideo, fill: lockedVideoFill) try engine.block.setContentFillMode(lockedVideo, mode: .cover) try engine.block.setDuration(lockedVideo, duration: 8) try engine.block.appendChild(to: page, child: lockedVideo) try engine.block.setPositionXMode(positionedVideo, mode: .absolute) try engine.block.setPositionYMode(positionedVideo, mode: .absolute) try engine.block.setPositionX(positionedVideo, value: 120) try engine.block.setPositionY(positionedVideo, value: 104) try engine.block.setFlipHorizontal(rotatedVideo, flip: true) try engine.block.scale(rotatedVideo, to: 1.15, anchorX: 0.5, anchorY: 0.5) try engine.block.setRotation(rotatedVideo, radians: .pi / 8) try engine.block.setWidth(lockedVideo, value: 280, maintainCrop: true) try engine.block.setHeight(lockedVideo, value: 158, maintainCrop: true) if try engine.block.supportsCrop(croppedVideo) { try engine.block.setContentFillMode(croppedVideo, mode: .crop) try engine.block.setCropScaleRatio(croppedVideo, scaleRatio: 1.35) // Crop translations are relative to the block frame dimensions. try engine.block.setCropTranslationX(croppedVideo, translationX: -0.12) try engine.block.setCropTranslationY(croppedVideo, translationY: 0.08) try engine.block.setCropRotation(croppedVideo, rotation: .pi / 18) try engine.block.adjustCropToFillFrame(croppedVideo, minScaleRatio: 1.0) } try engine.editor.setSettingBool("controlGizmo/showMoveHandles", value: true) try engine.editor.setSettingBool("controlGizmo/showResizeHandles", value: true) try engine.editor.setSettingBool("controlGizmo/showScaleHandles", value: true) try engine.editor.setSettingBool("controlGizmo/showRotateHandles", value: true) try engine.editor.setSettingBool("controlGizmo/showCropHandles", value: true) try engine.editor.setSettingFloat("controlGizmo/blockScaleDownLimit", value: 12) try engine.editor.setSettingEnum("touch/rotateAction", value: "Rotate") try engine.editor.setSettingEnum("touch/pinchAction", value: "Scale") if try engine.block.isGroupable([positionedVideo, croppedVideo]) { let group = try engine.block.group([positionedVideo, croppedVideo]) try engine.block.setPositionX(group, value: 180) try engine.block.setRotation(group, radians: .pi / 16) } if try engine.block.supportsAnimation(rotatedVideo) { let loopAnimation = try engine.block.createAnimation(.spinLoop) try engine.block.setLoopAnimation(rotatedVideo, animation: loopAnimation) try engine.block.setDuration(loopAnimation, duration: 2) try engine.block.setTimeOffset(rotatedVideo, offset: 1) } let blockFrameScopes = ["layer/move", "layer/rotate", "layer/resize", "layer/flip"] for scope in blockFrameScopes + ["layer/crop"] { try engine.editor.setGlobalScope(key: scope, value: .defer) } for scope in blockFrameScopes { try engine.block.setScopeEnabled(lockedVideo, key: scope, enabled: false) } try engine.block.setScopeEnabled(lockedVideo, key: "layer/crop", enabled: false) try engine.block.setTransformLocked(lockedVideo, locked: true) let transformsLocked = try engine.block.isTransformLocked(lockedVideo) let moveScopeEnabled = try engine.block.isScopeEnabled(lockedVideo, key: "layer/move") let moveAllowed = try engine.block.isAllowedByScope(lockedVideo, key: "layer/move") print("transformsLocked=\(transformsLocked) moveScopeEnabled=\(moveScopeEnabled) moveAllowed=\(moveAllowed)") } ``` Transform video blocks by moving, rotating, scaling, cropping, grouping, and locking them with the CE.SDK engine. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-video-transform) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Video transformations affect either the block frame or the media inside that frame. Block-level transforms change the graphic block's position, rotation, flip state, size, and group placement. Content-level crop transforms reframe the video fill without moving the block itself. | Transform layer | Use it for | Main APIs | | --- | --- | --- | | Block | Move, rotate, flip, scale, and resize the video block in the scene. | `setPositionX`, `setRotation`, `setFlipHorizontal`, `scale`, `setWidth` | | Content | Pan, zoom, or rotate the video inside the block frame. | `setCropScaleRatio`, `setCropTranslationX`, `setCropRotation`, `adjustCropToFillFrame` | | Permissions | Restrict end-user edits, crop reframing, or block-frame geometry changes. | `setScopeEnabled`, `setTransformLocked` | The snippets below run on the `@MainActor` — every engine call must happen on the main thread — and assume each named variable refers to a video graphic block in the current scene. ## Apply Block Transforms Block transforms affect the whole graphic block. Use absolute or percentage position modes for placement, radians for rotation, booleans for flip states, and an anchor point when scaling. ```swift highlight-videoTransform-blockTransforms try engine.block.setPositionXMode(positionedVideo, mode: .absolute) try engine.block.setPositionYMode(positionedVideo, mode: .absolute) try engine.block.setPositionX(positionedVideo, value: 120) try engine.block.setPositionY(positionedVideo, value: 104) try engine.block.setFlipHorizontal(rotatedVideo, flip: true) try engine.block.scale(rotatedVideo, to: 1.15, anchorX: 0.5, anchorY: 0.5) try engine.block.setRotation(rotatedVideo, radians: .pi / 8) try engine.block.setWidth(lockedVideo, value: 280, maintainCrop: true) try engine.block.setHeight(lockedVideo, value: 158, maintainCrop: true) ``` Pass `maintainCrop: true` when resizing a video block and you want the engine to adjust crop values so the visible content stays framed. ## Adjust the Video Inside the Frame Crop transforms change how the video fill appears inside the block frame. They do not change the block's own position or dimensions. ```swift highlight-videoTransform-contentTransforms if try engine.block.supportsCrop(croppedVideo) { try engine.block.setContentFillMode(croppedVideo, mode: .crop) try engine.block.setCropScaleRatio(croppedVideo, scaleRatio: 1.35) // Crop translations are relative to the block frame dimensions. try engine.block.setCropTranslationX(croppedVideo, translationX: -0.12) try engine.block.setCropTranslationY(croppedVideo, translationY: 0.08) try engine.block.setCropRotation(croppedVideo, rotation: .pi / 18) try engine.block.adjustCropToFillFrame(croppedVideo, minScaleRatio: 1.0) } ``` Crop translations are scaled offsets relative to the block frame, not pixel or design-unit positions. A `translationX` value of `-0.12` shifts the video content left by 12% of the frame width, while a `translationY` value of `0.08` shifts it down by 8% of the frame height. Positive X moves content right, and positive Y moves content down. Check `supportsCrop(_:)` before applying crop transforms if your code handles mixed block types. `adjustCropToFillFrame(_:minScaleRatio:)` prevents empty frame areas after crop scale, translation, or rotation changes. ## Configure Transform Controls The engine exposes interaction settings that an editor built on the engine reads to decide which handles appear and how touch gestures map to transforms. ```swift highlight-videoTransform-transformControls try engine.editor.setSettingBool("controlGizmo/showMoveHandles", value: true) try engine.editor.setSettingBool("controlGizmo/showResizeHandles", value: true) try engine.editor.setSettingBool("controlGizmo/showScaleHandles", value: true) try engine.editor.setSettingBool("controlGizmo/showRotateHandles", value: true) try engine.editor.setSettingBool("controlGizmo/showCropHandles", value: true) try engine.editor.setSettingFloat("controlGizmo/blockScaleDownLimit", value: 12) try engine.editor.setSettingEnum("touch/rotateAction", value: "Rotate") try engine.editor.setSettingEnum("touch/pinchAction", value: "Scale") ``` `controlGizmo/blockScaleDownLimit` uses screen pixels; the value above keeps scaled blocks at least 12 screen pixels wide and high. Programmatic transform calls use the block APIs directly and ignore these settings. Scope checks for those block APIs are disabled by default unless `debug/enforceScopesInAPIs` is enabled, while block-frame transform locks are enforced separately by the block-frame transform APIs. ## Transform Groups Group multiple blocks when they should move or rotate together while preserving their relative placement. ```swift highlight-videoTransform-groupTransforms if try engine.block.isGroupable([positionedVideo, croppedVideo]) { let group = try engine.block.group([positionedVideo, croppedVideo]) try engine.block.setPositionX(group, value: 180) try engine.block.setRotation(group, radians: .pi / 16) } ``` Call `isGroupable(_:)` before grouping arbitrary selections. A group receives its own block ID, so you can transform the group with the same block APIs used for single blocks. ## Animate and Time Transforms Video scenes can combine static transforms with block animations and timeline placement. Attach animation blocks to the transformed video block and set the block's time offset when it should appear later in the page timeline. ```swift highlight-videoTransform-animatedTransforms if try engine.block.supportsAnimation(rotatedVideo) { let loopAnimation = try engine.block.createAnimation(.spinLoop) try engine.block.setLoopAnimation(rotatedVideo, animation: loopAnimation) try engine.block.setDuration(loopAnimation, duration: 2) try engine.block.setTimeOffset(rotatedVideo, offset: 1) } ``` Use the dedicated animation guides when you need keyframe-style motion, easing, or detailed animation property configuration. ## Restrict Transform Changes For templates, disable individual transform scopes and use the transform lock for block-frame geometry. Block-level scope flags take effect when the matching global scope is set to `.defer`; use `isAllowedByScope(_:key:)` to read the effective permission. ```swift highlight-videoTransform-lockTransforms let blockFrameScopes = ["layer/move", "layer/rotate", "layer/resize", "layer/flip"] for scope in blockFrameScopes + ["layer/crop"] { try engine.editor.setGlobalScope(key: scope, value: .defer) } for scope in blockFrameScopes { try engine.block.setScopeEnabled(lockedVideo, key: scope, enabled: false) } try engine.block.setScopeEnabled(lockedVideo, key: "layer/crop", enabled: false) try engine.block.setTransformLocked(lockedVideo, locked: true) let transformsLocked = try engine.block.isTransformLocked(lockedVideo) let moveScopeEnabled = try engine.block.isScopeEnabled(lockedVideo, key: "layer/move") let moveAllowed = try engine.block.isAllowedByScope(lockedVideo, key: "layer/move") print("transformsLocked=\(transformsLocked) moveScopeEnabled=\(moveScopeEnabled) moveAllowed=\(moveAllowed)") ``` Use individual scopes when one operation should remain available, such as allowing crop but preventing movement. Disable `layer/crop` when crop or content reframing must stay fixed. `setTransformLocked(_:locked:)` protects block-frame geometry — moving, rotating, flipping, scaling, and resizing a locked block throws — while crop setters use the `layer/crop` permission path instead. A lock applies down the hierarchy, so a child of a locked group cannot be transformed either. The lock covers the static transform: keyframe tracks on the position, rotation, size, scale, anchor or skew still animate a locked block. Each block keeps its own flag, so unlocking the group leaves a child that was locked on its own still locked. A page is the exception: locking a page holds the page and not what sits on it. `isScopeEnabled(_:key:)` reads only the block-level flag, while `isAllowedByScope(_:key:)` combines the global and block-level scope state. Engine API calls from your app code are not blocked by scopes unless `debug/enforceScopesInAPIs` is enabled, so gate the controls your app exposes on `isAllowedByScope(_:key:)`. ## Troubleshooting | Issue | Check | | --- | --- | | A block does not move or rotate | For editor interactions, confirm the block is valid and `isAllowedByScope(_:key:)` returns `true` for the needed scope. For block API calls, also check whether `debug/enforceScopesInAPIs` is enabled and `isTransformLocked(_:)` is `false`. | | Position values look wrong | Verify whether the block uses `PositionMode.absolute` or `PositionMode.percent`. | | Rotation uses the wrong angle | Pass radians, not degrees. | | Crop leaves empty frame areas | Call `adjustCropToFillFrame(_:minScaleRatio:)` after changing crop scale, translation, or rotation. | | UI handles are missing | Check the `controlGizmo/*` settings and ensure the selected block supports the requested operation. | ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.setPositionX(_:value:)` | Set the block's x position relative to its parent. | | `engine.block.setPositionY(_:value:)` | Set the block's y position relative to its parent. | | `engine.block.setPositionXMode(_:mode:)` | Choose absolute or percentage x positioning with `PositionMode`. | | `engine.block.setPositionYMode(_:mode:)` | Choose absolute or percentage y positioning with `PositionMode`. | | `engine.block.getPositionX(_:)` / `getPositionY(_:)` | Read the block's current x and y position. | | `engine.block.setRotation(_:radians:)` | Rotate the block around its center. | | `engine.block.getRotation(_:)` | Read the block's rotation in radians. | | `engine.block.setFlipHorizontal(_:flip:)` / `setFlipVertical(_:flip:)` | Mirror the block horizontally or vertically. | | `engine.block.isFlipHorizontal(_:)` / `isFlipVertical(_:)` | Check whether the block is mirrored. | | `engine.block.scale(_:to:anchorX:anchorY:)` | Scale the block around a normalized anchor point. | | `engine.block.setWidth(_:value:maintainCrop:)` / `setHeight(_:value:maintainCrop:)` | Resize the block and optionally preserve crop framing. | | `engine.block.setContentFillMode(_:mode:)` | Choose `ContentFillMode.crop`, `.cover`, or `.contain` for the block's content. | | `engine.block.supportsCrop(_:)` | Check whether the block supports crop transforms before applying crop values. | | `engine.block.setCropScaleRatio(_:scaleRatio:)` | Uniformly scale the video content inside the frame. | | `engine.block.setCropScaleX(_:scaleX:)` / `setCropScaleY(_:scaleY:)` | Scale the video content per axis inside the frame. | | `engine.block.setCropTranslationX(_:translationX:)` / `setCropTranslationY(_:translationY:)` | Set the relative crop offset; `1.0` equals one frame dimension. | | `engine.block.setCropRotation(_:rotation:)` | Rotate the video content inside the block frame. | | `engine.block.adjustCropToFillFrame(_:minScaleRatio:)` | Adjust crop values so the content fills the block frame. | | `engine.block.getCropScaleRatio(_:)` / `getCropScaleX(_:)` / `getCropScaleY(_:)` / `getCropTranslationX(_:)` / `getCropTranslationY(_:)` / `getCropRotation(_:)` | Read the current crop values. | | `engine.block.resetCrop(_:)` | Reset manual crop values to their default state. | | `engine.block.isGroupable(_:)` | Check whether the selected blocks can be grouped. | | `engine.block.group(_:)` | Create a group that can be transformed as one block. | | `engine.block.ungroup(_:)` | Dissolve a group back into its member blocks. | | `engine.block.supportsAnimation(_:)` | Check whether a block can receive animations. | | `engine.block.createAnimation(_:)` | Create an animation block such as `AnimationType.spinLoop`. | | `engine.block.setInAnimation(_:animation:)` / `setLoopAnimation(_:animation:)` / `setOutAnimation(_:animation:)` | Attach an entry, looping, or exit animation to a block. | | `engine.block.setTimeOffset(_:offset:)` | Place the block later in its parent timeline, in seconds. | | `engine.block.setDuration(_:duration:)` | Set how long a page, video block, or animation block participates in the timeline. | | `engine.editor.setGlobalScope(key:value:)` | Set a global scope to `GlobalScope.allow`, `.deny`, or `.defer`. | | `engine.block.setScopeEnabled(_:key:enabled:)` | Enable or disable a block-level scope such as `"layer/move"`, `"layer/rotate"`, `"layer/flip"`, `"layer/resize"`, or `"layer/crop"`. | | `engine.block.isScopeEnabled(_:key:)` | Read only the block-level scope flag. | | `engine.block.isAllowedByScope(_:key:)` | Read the effective permission after global and block-level scope state combine. | | `engine.block.setTransformLocked(_:locked:)` / `isTransformLocked(_:)` | Lock, unlock, or check block-frame geometry transforms. Keyframe tracks still animate a locked block. | | `engine.editor.setSettingBool(_:value:)` | Toggle `controlGizmo/*` handle visibility in the editor interaction layer. | | `engine.editor.setSettingFloat(_:value:)` | Set `controlGizmo/blockScaleDownLimit`, the minimum on-screen block size while scaling. | | `engine.editor.setSettingEnum(_:value:)` | Map `touch/rotateAction` and `touch/pinchAction` gestures to transform behavior. | ## Next Steps - [Move](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/move-aa9d89/) — Position video blocks with absolute or percentage coordinates. - [Rotate](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/rotate-eaf662/) — Rotate video blocks with radians. - [Flip](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/flip-a603b0/) — Mirror video blocks horizontally or vertically. - [Scale](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/scale-f75c8a/) — Scale video blocks around an anchor point. - [Crop](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/crop-8b1741/) — Reframe video content inside a block. - [Resize](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/resize-b1ce14/) — Change video block dimensions while managing crop behavior. --- ## Related Pages - [Transform Overview](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/overview-22ed11/) - Learn how CE.SDK applies geometric and crop transformations in video scenes, when to use them, and how they relate to the dedicated transform guides. - [Move](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/move-aa9d89/) - Position a video relative to its parent using either percentage or units - [Crop Video](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/crop-8b1741/) - Cut out specific areas of a video to focus on key content or change aspect ratio - [Rotate](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/rotate-eaf662/) - Rotate video clips either freeform or by set angles - [Resize](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/resize-b1ce14/) - Change the frame size of individual elements or groups - [Scale](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/scale-f75c8a/) - Scale video clips and streams uniformly in projects - [Flip](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/flip-a603b0/) - Flip video clips horizontally or vertically or both --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Crop Video" description: "Cut out specific areas of a video to focus on key content or change aspect ratio" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/crop-8b1741/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform-369f28/) > [Crop](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/crop-8b1741/) --- Crop videos to focus on specific areas or remove unwanted edges using programmatic crop transforms. ![Cropped video frame after scaling, translating, rotating, and flipping](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-video-transform-crop) Video cropping in CreativeEditor SDK (CE.SDK) lets you re-frame clips and remove unwanted edges by moving the content inside the block. Unlike resizing or scaling which affects the entire frame uniformly, cropping selects a specific region of the source video to display inside the block's existing dimensions. ```swift file=@cesdk_swift_examples/engine-guides-create-video-transform-crop/CropVideo.swift reference-only import Foundation import IMGLYEngine @MainActor func cropVideo(engine: Engine) async throws { // Demo scaffolding: a video scene with a single video block that fills the page. let scene = try engine.scene.createVideo() let baseURL = try engine.guidesBaseURL let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.setDuration(page, duration: 5) let videoBlock = try engine.block.create(.graphic) try engine.block.setShape(videoBlock, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(videoBlock, fill: videoFill) let track = try engine.block.create(.track) try engine.block.appendChild(to: page, child: track) try engine.block.appendChild(to: track, child: videoBlock) try engine.block.fillParent(track) // Decode at least one video frame before exporting snapshots. try await engine.block.forceLoadAVResource(videoFill) let canCrop = try engine.block.supportsCrop(videoBlock) _ = canCrop // Center-crop: scale both axes uniformly while keeping the content centered. try engine.block.setCropScaleRatio(videoBlock, scaleRatio: 1.5) try await engine.captureGuide(page, label: "after-scale") // Or scale each axis independently. Unequal values stretch the content. try engine.block.setCropScaleX(videoBlock, scaleX: 1.5) try engine.block.setCropScaleY(videoBlock, scaleY: 2.0) // Pan the content within the frame. Values are normalized fractions of the // frame dimensions: 0.25 moves the content one quarter of the frame to the right. try engine.block.setCropTranslationX(videoBlock, translationX: 0.25) try engine.block.setCropTranslationY(videoBlock, translationY: -0.1) try await engine.captureGuide(page, label: "after-translate") // Rotate the content within the crop frame. Rotation is in radians. try engine.block.setCropRotation(videoBlock, rotation: .pi / 6) let scaleRatio = try engine.block.getCropScaleRatio(videoBlock) let scaleX = try engine.block.getCropScaleX(videoBlock) let scaleY = try engine.block.getCropScaleY(videoBlock) let rotation = try engine.block.getCropRotation(videoBlock) let offsetX = try engine.block.getCropTranslationX(videoBlock) let offsetY = try engine.block.getCropTranslationY(videoBlock) _ = (scaleRatio, scaleX, scaleY, rotation, offsetX, offsetY) // After translating or rotating you can re-fill the frame to remove letterboxing. try engine.block.adjustCropToFillFrame(videoBlock, minScaleRatio: 1.0) // Mirror the content along the vertical axis. try engine.block.flipCropHorizontal(videoBlock) try await engine.captureGuide(page, label: "hero") try engine.block.setCropAspectRatioLocked(videoBlock, locked: true) let isLocked = try engine.block.isCropAspectRatioLocked(videoBlock) _ = isLocked // Reset every crop transform back to its starting state. try engine.block.resetCrop(videoBlock) } ``` This guide covers the programmatic crop API: checking crop support, scaling and translating the content, rotating and flipping it, locking the aspect ratio, and resetting the transform. ## Check Crop Support Before applying crop operations, verify the block supports cropping with `engine.block.supportsCrop(_:)`. Graphic blocks with image or video fills return `true`: ```swift highlight-cropVideo-checkSupport let canCrop = try engine.block.supportsCrop(videoBlock) ``` ## Scale Crop Use `engine.block.setCropScaleRatio(_:scaleRatio:)` to scale the video content uniformly within its frame. Values greater than `1.0` zoom in, values less than `1.0` zoom out, and the transform keeps the content centered: ```swift highlight-cropVideo-scale // Center-crop: scale both axes uniformly while keeping the content centered. try engine.block.setCropScaleRatio(videoBlock, scaleRatio: 1.5) ``` If you need to scale each axis independently — for example to stretch the content — call `setCropScaleX` and `setCropScaleY` directly. Unequal values distort the video: ```swift highlight-cropVideo-scaleAxis // Or scale each axis independently. Unequal values stretch the content. try engine.block.setCropScaleX(videoBlock, scaleX: 1.5) try engine.block.setCropScaleY(videoBlock, scaleY: 2.0) ``` ## Translate Crop Pan the video content within the crop frame with `engine.block.setCropTranslationX(_:translationX:)` and `engine.block.setCropTranslationY(_:translationY:)`. Translation values are normalized fractions of the frame dimensions, so `0.25` moves the content one quarter of the frame to the right and `-0.1` moves it 10% up: ```swift highlight-cropVideo-translate // Pan the content within the frame. Values are normalized fractions of the // frame dimensions: 0.25 moves the content one quarter of the frame to the right. try engine.block.setCropTranslationX(videoBlock, translationX: 0.25) try engine.block.setCropTranslationY(videoBlock, translationY: -0.1) ``` ## Rotate Crop Rotate the video content within its frame using `engine.block.setCropRotation(_:rotation:)`. Rotation is specified in radians where `.pi` equals 180 degrees: ```swift highlight-cropVideo-rotate // Rotate the content within the crop frame. Rotation is in radians. try engine.block.setCropRotation(videoBlock, rotation: .pi / 6) ``` ## Get Crop Values Read the current crop state with the matching getters. The example below captures the scale ratio, the per-axis scales, the rotation, and the translation offsets: ```swift highlight-cropVideo-getValues let scaleRatio = try engine.block.getCropScaleRatio(videoBlock) let scaleX = try engine.block.getCropScaleX(videoBlock) let scaleY = try engine.block.getCropScaleY(videoBlock) let rotation = try engine.block.getCropRotation(videoBlock) let offsetX = try engine.block.getCropTranslationX(videoBlock) let offsetY = try engine.block.getCropTranslationY(videoBlock) ``` ## Fill Frame Translations and rotations can reveal the empty area behind the video as letterboxing. Call `engine.block.adjustCropToFillFrame(_:minScaleRatio:)` to automatically adjust the scale and translation so the content covers the full frame. The `minScaleRatio` argument sets the minimum scale the engine is allowed to settle on: ```swift highlight-cropVideo-fillFrame // After translating or rotating you can re-fill the frame to remove letterboxing. try engine.block.adjustCropToFillFrame(videoBlock, minScaleRatio: 1.0) ``` ## Flip Crop Flip the content horizontally or vertically within its crop frame with `engine.block.flipCropHorizontal(_:)` and `engine.block.flipCropVertical(_:)`. These flip the *content*, not the block on the canvas — every call toggles the orientation, so calling the same function twice returns the content to its original state: ```swift highlight-cropVideo-flip // Mirror the content along the vertical axis. try engine.block.flipCropHorizontal(videoBlock) ``` ## Lock Aspect Ratio Lock the crop's aspect ratio during interactive editing with `engine.block.setCropAspectRatioLocked(_:locked:)`. When locked, crop handles in the editor maintain the current aspect ratio while the user drags. Use `engine.block.isCropAspectRatioLocked(_:)` to query the current state: ```swift highlight-cropVideo-lockAspect try engine.block.setCropAspectRatioLocked(videoBlock, locked: true) let isLocked = try engine.block.isCropAspectRatioLocked(videoBlock) ``` ## Reset Crop Reset every crop transform back to the engine's initial values using `engine.block.resetCrop(_:)`. The engine restores the scale and translation that were applied when the video was first placed inside the block: ```swift highlight-cropVideo-reset // Reset every crop transform back to its starting state. try engine.block.resetCrop(videoBlock) ``` ## Coordinate System Crop transforms operate in normalized units rather than pixels: | Property | Value Type | Description | | --- | --- | --- | | Scale | `Float` (0.0+) | `1.0` is original size, `2.0` is double, `0.5` is half | | Translation | `Float` | Fraction of the frame dimensions; `1.0` shifts by a full frame | | Rotation | `Float` (radians) | `.pi` equals 180°, `.pi / 2` equals 90° | All crop values are independent of the canvas zoom level and the timeline duration — cropping changes the visual framing of the clip for its entire duration but does not trim time. ## Combining with Other Transforms Crop transforms move the content inside the block; the block's own position, rotation, and scale move the block on the canvas. The two are independent — you can chain them freely. For example, after scaling and rotating the crop you can change the block's own rotation and width with `engine.block.setRotation(_:radians:)` and `engine.block.setWidth(_:value:)`. The order of crop calls matters: rotating first and then scaling does not produce the same frame as scaling first and then rotating. ## Troubleshooting ### Crop functions throw a scope error Crop functions require the `layer/crop` scope. The default *Creator* role has it enabled; if you've switched to a more restrictive role, re-enable the scope before calling the crop API. ### Crop handles are not visible Confirm the selected block has a video fill. Crop handles only appear for blocks whose fill type supports cropping. Check that `controlGizmo/showCropHandles` is still enabled in your engine settings. ### Black bars after scaling or translating Call `engine.block.adjustCropToFillFrame(_:minScaleRatio:)` so the engine re-fits the content to the frame, or raise the scale ratio until the content fully covers the block. ## API Reference | Method | Description | | --- | --- | | `engine.block.supportsCrop(_:)` | Check if a block supports cropping | | `engine.block.setCropScaleRatio(_:scaleRatio:)` | Set the uniform scale ratio | | `engine.block.setCropScaleX(_:scaleX:)` | Set the horizontal scale | | `engine.block.setCropScaleY(_:scaleY:)` | Set the vertical scale | | `engine.block.setCropTranslationX(_:translationX:)` | Set the horizontal pan | | `engine.block.setCropTranslationY(_:translationY:)` | Set the vertical pan | | `engine.block.setCropRotation(_:rotation:)` | Set the rotation in radians | | `engine.block.getCropScaleRatio(_:)` | Read the current scale ratio | | `engine.block.getCropScaleX(_:)` | Read the current horizontal scale | | `engine.block.getCropScaleY(_:)` | Read the current vertical scale | | `engine.block.getCropTranslationX(_:)` | Read the current horizontal translation | | `engine.block.getCropTranslationY(_:)` | Read the current vertical translation | | `engine.block.getCropRotation(_:)` | Read the current rotation | | `engine.block.adjustCropToFillFrame(_:minScaleRatio:)` | Auto-adjust to fill the frame | | `engine.block.flipCropHorizontal(_:)` | Flip content horizontally | | `engine.block.flipCropVertical(_:)` | Flip content vertically | | `engine.block.setCropAspectRatioLocked(_:locked:)` | Lock or unlock the aspect ratio | | `engine.block.isCropAspectRatioLocked(_:)` | Check whether the aspect ratio is locked | | `engine.block.resetCrop(_:)` | Reset every crop transform | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Flip Videos" description: "Flip videos horizontally or vertically to create mirror effects and symmetrical designs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/flip-a603b0/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform-369f28/) > [Flip](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/flip-a603b0/) --- Video flipping in CreativeEditor SDK (CE.SDK) allows you to mirror video content horizontally or vertically. This transformation is useful for creating symmetrical designs, correcting orientation issues, or achieving specific visual effects in your video projects. You can flip videos both through the built-in user interface and programmatically using the SDK's APIs, providing flexibility for different workflow requirements. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## Available Flip Operations CE.SDK supports two types of video flipping: - **Horizontal Flip**: Mirror the video along its vertical axis, creating a left-right reflection - **Vertical Flip**: Mirror the video along its horizontal axis, creating a top-bottom reflection These operations can be applied individually or combined to achieve the desired visual effect. ## Applying Flips ### UI-Based Flipping You can apply flips directly in the CE.SDK user interface. The editor provides intuitive controls for horizontally and vertically flipping videos, making it easy for users to quickly mirror content without writing code. ### Programmatic Flipping Developers can also apply flips programmatically, using the SDK's API. This allows for dynamic video adjustments based on application logic, user input, or automated processes. ## Combining with Other Transforms Video flipping works seamlessly with other transformation operations like rotation, scaling, and cropping. You can chain multiple transformations to create complex visual effects while maintaining video quality. ## Guides --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Move" description: "Position a video relative to its parent using either percentage or units" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/move-aa9d89/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform-369f28/) > [Move](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/move-aa9d89/) --- This guide shows how to move video blocks on the canvas using CE.SDK in your app. You’ll learn how to reposition single elements, move groups, and constrain movement behavior within templates. You can move elements programmatically or by using the built-in IMG.LY UI editors. ## What You’ll Learn - Move video programmatically using Swift - Use the IMG.LY UI to drag images - Adjust video position on the canvas - Move multiple blocks together - Constrain video movement in templates ## When to Use Use movement to: - Position content precisely in designs - Align video with text, backgrounds, or grid layouts - Enable drag-and-drop or animated movement workflows *** ## Move Videos With the UI Users can drag and drop elements directly in the editor canvas. *** ## Move a Video Block Programmatically Video block position is controlled using the `position/x` and `position/y` properties. They can either use absolute or percentage (relative) values. In addition to setting the properties, there are helper functions. ```swift try engine.block.setFloat(videoBlock, property: "position/x", value: 150) try engine.block.setFloat(videoBlock, property: "position/y", value: 100) ``` or ```swift try engine.block.setPositionX(videoBlock, value: 150) try engine.block.setPositionY(videoBlock, value: 150) ``` This moves the video to coordinates (150, 100) on the canvas. The origin point (0, 0) is at the top-left. ```swift try engine.block.setPositionXMode(videoBlock, mode: .percent) try engine.block.setPositionYMode(videoBlock, mode: .percent) try engine.block.setPositionX(videoBlock, value: 0.5) try engine.block.setPositionY(videoBlock, value: 0.5) ``` This moves the video to the center of the canvas, regardless of the dimensions of the canvas. As with setting position, you can update or check the mode using `position/x/mode` and `position/y/mode` properties. ```swift let xPosition = try engine.block.getPositionX(videoBlock) let yPosition = try engine.block.getPositionY(videoBlock) ``` *** ## Move Multiple Elements Together Group elements before moving to keep them aligned: ```swift let groupId = try engine.block.group([videoBlockId, textBlockId]) try engine.block.setPositionX(groupId, value: 200) ``` This moves the entire group to 200 from the left edge. *** ## Move Rrelative to Current Position To nudge a video instead of setting an absolute position: ```swift let xPosition = try engine.block.getPositionX(videoBlock) try engine.block.setPositionX(videoBlock, value: xPosition + 20) ``` This moves the video 20 points to the right. *** ## Lock Movement (optional) When building templates, you might want to lock movement to protect the layout: ```swift try engine.block.setScopeEnabled(videoBlock, key: "layer/move", enabled: false) ``` You can also disable all transformations for a block by locking, this is regardless of working with a template. ```swift try engine.block.setTransformLocked(videoBlock, locked: true) ``` *** ## Troubleshooting | Issue | Solution | | ------------------------ | ----------------------------------------------------- | | Video block not moving | Ensure it is not constrained or locked | | Unexpected position | Check canvas coordinates and alignment settings | | Grouped items misaligned | Confirm all items share the same reference point | | Can’t move via UI | Ensure the move feature is enabled in the UI settings | *** --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Transform Overview" description: "Learn how CE.SDK applies geometric and crop transformations in video scenes, when to use them, and how they relate to the dedicated transform guides." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/overview-22ed11/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform-369f28/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/overview-22ed11/) --- Transforms control where a video block sits in the composition and how its content is framed. In CE.SDK, there are two families of transforms: - **Block-level transforms** change the block container (position, rotation, size, flip). - **Content-level transforms** (the `crop*` family) control how the video fills that container (pan, zoom, and content rotation). This overview explains the mental model and points you to focused sub‑guides for implementation details. ## What you’ll learn - The difference between **block** and **content (crop)** transforms. - Coordinate systems, anchor points, and units used in transforms. - Essential APIs & UI toggles for transform workflows. - How to restrict or lock transformations in templates. - Troubleshooting common issues. ## Understanding Transform Layers Each block in a scene has a transform that determines within the page: - The block’s position - Its rotation - Its scale Changing these values moves or rotates the block **relative to its parent**. When the block has a **video fill**, a second, content-level transform applies to the media *inside* the block. Use it to crop, pan, and zoom without changing the block’s geometry. In CE.SDK, all properties and methods beginning with `crop` belong to the **content-level transform**, such as: - `setCropScaleX`, `setCropScaleY` - `setCropRotation` - `setCropTranslationX`, `setCropTranslationY` These control how the video fills the block’s frame rather than altering the block’s geometry itself. Use `crop` methods for these actions within a clip: - Reframing - Punch-in Use **block transforms** to apply actions within a scene: - Moving - Rotating ![Block vs Content Transforms](assets/transform-overview-rotate.png) The preceding diagram compares block-level and content-level transforms: - The **left** frame shows a block rotated. - The **right** frame shows crop rotation of the video within its block frame. ## Coordinate Systems and Units - **Position:** absolute or percentage modes. - **Rotation:** radians (positive = counterclockwise). - **Scale:** uniform; optional anchor in normalized 0–1 space (0=left/top, 0.5=center, 1=right/bottom). - **Crop translation/scale:** normalized so pan/zoom behave consistently across resolutions. - **UI vs API spaces:** gizmo movements operate in canvas/screen space, while API values are normalized or scene‑space; always verify the mode before mixing. *** ## Built-in Transform UI The editor provides optional gizmos and gestures for direct manipulation: - Handles: `controlGizmo/showRotateHandles`, `/showResizeHandles`, `/showMoveHandles`, `/showScaleHandles`, `/showCropHandles`. - Gestures: `touch/rotateAction`, `touch/pinchAction`. - Limits: `controlGizmo/blockScaleDownLimit` prevents accidental shrinking to zero size. These can be read or set using: ```swift try engine.editor.setSettingBool(key: "controlGizmo/showRotateHandles", value: true) try engine.editor.setSettingFloat(key: "controlGizmo/blockScaleDownLimit", value: 0.4) ``` **Defaults:** If not configured, the editor exposes a safe, minimal set of handles; crop handles only appear when the selected block is croppable. ## Transform API Map (Quick Reference) | Action | Methods | |--------|----------| | Move | `setPositionX`, `setPositionY`, `setPositionXMode`, `setPositionYmode` | | Rotate | `setRotation` | | Flip | `setFlipHorizontal`, `setFlipVertical` | | Scale | `scale` | | Resize | `setWidth`, `setHeight`, `setWidthMode`, `setHeightMode` | | Crop | `setCropRotation`, `setCropScaleRatio`, `setCropTranslationX/Y`, `adjustCropToFillFrame`, `resetCrop` | See the dedicated sub‑guides for full examples and edge cases. ## Animated Transforms All transform properties in CE.SDK can be animated over time in video scenes. You can keyframe changes to create: - Movement - Zooms - Transitions - Keyframes live on the **timeline** associated with each block. - Interpolation curves (easing) control how values change between keys. - Programmatic animation uses standard transform methods but attached to timeline events. - **Crop UI gating:** Crop handles appear only when a selected block is croppable (e.g., a `.video` fill). - **Performance:** Transforms are GPU‑accelerated at playback; avoid heavy, stacked effects ahead of transform‑driven motion. The [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) guide shows how to add keyframes, adjust easing, and preview animations. *** ## Permissions and Locking You can restrict or disable transforms in templates to prevent accidental edits: ```swift try engine.block.setScopeEnabled(blockID, key: "layer/rotate", enabled: false) try engine.block.setTransformLocked(blockID, locked: true) ``` These controls affect both UI gestures and API calls. ## Troubleshooting | Issue | Possible Cause | |--------|----------------| | Rotation appears wrong | Check radians vs degrees | | Block not responding | Transform locked or scope disabled | | Crop handles missing | Ensure block fill is croppable (e.g., `.video`) | | Unexpected scaling | Verify anchor and percentage mode | | Transforming group has no effect | Ensure blocks are grouped correctly | ## Next Steps Learn specific transformation APIs: - [Move](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/move-aa9d89/) - [Rotate](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/rotate-eaf662/) - [Flip](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/flip-a603b0/) - [Scale](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/scale-f75c8a/) - [Resize](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/resize-b1ce14/) Related: - [Create Video: Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) - [Animation Overview](https://img.ly/docs/cesdk/mac-catalyst/animation/overview-6a2ef2/) - [Group Elements](https://img.ly/docs/cesdk/mac-catalyst/create-composition/group-and-ungroup-62565a/) --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Resize Videos" description: "Change the dimensions of video elements to fit specific layout requirements." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/resize-b1ce14/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform-369f28/) > [Resize](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/resize-b1ce14/) --- Video resizing in CreativeEditor SDK (CE.SDK) allows you to change the dimensions of video elements to match specific layout requirements. Unlike scaling, resizing allows independent control of width and height dimensions, making it ideal for fitting videos into predefined spaces or responsive layouts. You can resize videos both through the built-in user interface and programmatically using the SDK's APIs, providing flexibility for different workflow requirements. [Launch Web Demo](https://img.ly/showcases/cesdk) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## Resize Methods CE.SDK supports several approaches to video resizing: - **Absolute Dimensions**: Set specific pixel dimensions for precise control - **Percentage-based Resizing**: Size relative to parent container for responsive designs - **UI Resize Handles**: Interactive resize controls in the editor interface - **Aspect Ratio Constraints**: Maintain or ignore aspect ratios during resize operations ## Applying Resizing ### UI-Based Resizing You can resize videos directly in the CE.SDK user interface using resize handles. Users can drag edge and corner handles to adjust dimensions independently or proportionally, making it easy to fit videos into specific layouts visually. ### Programmatic Resizing Developers can apply resizing programmatically, using the SDK's API. This allows for precise dimension control, automated layout adjustments, and integration with responsive design systems or template constraints. ## Combining with Other Transforms Video resizing works seamlessly with other transformation operations like rotation, cropping, and positioning. You can chain multiple transformations to create complex layouts while maintaining video quality and performance. ## Guides --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Rotate" description: "Rotate video clips either freeform or by set angles" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/rotate-eaf662/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform-369f28/) > [Rotate](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/rotate-eaf662/) --- Learn how to programmatically and interactively rotate videos in your iOS app using CE.SDK. This guide walks you through rotating video blocks in an editor, using Swift to rotate, and giving your users intuitive controls and ensuring predictable editing behavior. ## What you'll learn - How to rotate a video as a user using the handles - Rotate a video block by a specific angle - How to lock video rotation - How to rotate multiple images as a group ### Rotating a Video Using the UI By default selecting a block will show handles for resizing. You can freeform rotate a block using a standard two-finger rotation gesture. To give the user the rotation handles, set the `editor` configuration setting. ```swift try engine.editor.setSettingBool("controlGizmo/showRotateHandles", value: true) ``` When working with an editor, it's best to modify settings in the `EditorConfiguration.onCreate` callback. When working with the engine directly, they can be set at any time. ![Rotation handle of the control gizmo enabled for a video block](../mobile-assets/rotate-example-1.png) Use the `Crop` tab to rotate a video up to 45 degrees using the sliding control and in 90 degree increments using the rotation button. ![Crop menu showing rotation slider and button](../mobile-assets/rotate-example-2.png) > **Note:** Notice that using the Crop menu rotates the video, but not the block > containing the video. ### Rotating a view using code You can rotate a video block using the `setRotation` function. It takes the `id` of the block and a rotation amount in radians. Positive rotation values rotate **counterclockwise**. ```swift try engine.block.setRotation(videoBlock, radians: .pi / 4) ``` > **Note:** This rotates the entire block. If you want to rotate a video that is filling a > block but not the block, explore the > [crop rotate](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/crop-8b1741/) function. If you need to convert between radians and degrees, multiply the number in degrees by pi and divide by 180. ```swift let angleInRadians: Double = angleInDegrees * Double.pi / 180 let angleInDegrees: Double = angleInRadians * 180 / Double.pi ``` You can discover the current rotation of a block using the `getRotation` function. ```swift let rotationOfClip= try engine.block.getRotation(videoBlock) ``` ### Rotating as a group To rotate multiple elements together, first add them to a `group` and then rotate the group. ```swift let groupId = try engine.block.group([videoBlock, textBlock]) engine.block.setRotation(groupId, radians: pi / 2) ``` ### Locking rotation You can remove the rotation handle from the UI by changing the setting for the engine. This will affect *all* blocks. ```swift try engine.editor.setSettingBool("controlGizmo/showRotateHandles", value: false) ``` Though the handle is gone, the user can still use the two finger rotation gesture on a touch device. You can disable that gesture with the following setting. ```swift try engine.editor.setSettingEnum("touch/rotateAction", value: "None") ``` When you want to lock only certain blocks, you can toggle the transform lock property. This will apply to all transformations for the block. ```swift try engine.block.setTransformLocked(videoBlock, locked: true) ``` When working with templates, you can lock a block from rotating by setting its scope. Remember that the global layer has to defer to the blocks using `setGlobalScope`. ```swift try engine.block.setScopeEnabled(imageBlock, key: "layer/rotate", enabled: false) ``` ### Troubleshooting Troubleshooting | Issue | Solution | | ----------------------------------- | ------------------------------------------------------------------------------- | | Video appears offset after rotation | Make sure the pivot point is centered (default is center). | | Rotation not applying | Confirm that the video block is inserted and rendered before applying rotation. | | Rotation handle not visible | Check that interactive UI controls are enabled in the settings. | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Scale" description: "Scale video clips and streams uniformly in projects" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/scale-f75c8a/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Transform](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform-369f28/) > [Scale](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/scale-f75c8a/) --- This guide shows you how to scale video clips using CE.SDK in your iOS project. You'll learn how to scale video blocks proportionally, scale groups and apply scaling constraints to protect template structure. Because of the CE.SDK block architecture, many of the commands and concepts apply to all types of graphical fills. Methods for scaling video work the same when scaling images, text and other types of blocks. ## What you'll learn - Scale video using the UI - Scale video programmatically using Swift - Scale proportionally or non-uniformly - Scale grouped elements - Apply scale constraints in templates ## When to use Use scaling to: - Emphasize or de-emphasize elements - Fit video to available space without cropping - Enable pinch-to-zoom gestures or dynamic layouts *** ### Scale video using the UI When using an editor such as the **Video Editor** there are two methods for scaling video clip blocks, touch controls or the `Crop` menu. CE.SDK supports the standard pinch-to-zoom gesture for scaling. Scaling using the touch controls changes the scale of the entire video block. Scaling in the `Crop` menu changes the scale of the underlying video, but leaves the block's scale unchanged. Learn more about scaling while cropping in the [Crop guide](https://img.ly/docs/cesdk/mac-catalyst/edit-video/transform/crop-8b1741/). The **Video Editor** also has a `Resize` menu, but those settings are for resizing the entire scene, not individual clips. ![Example of scaling the size of a block and crop scaling the underlying video](../mobile-assets/scale-example-2.png) ## Scale video programmatically using Swift ### Scale uniformly Scaling uses the `scale(_ id: DesignBlockID, to scale: Float)` function. A scale value of `1.0` is the original scale. Values larger than `1.0` increase the scale of the block and values lower than `1.0` scale the block smaller. A value of `2.0`, for example makes the block twice as large. This scales the video to 150% of its original size. The origin anchor point is unchanged, so the image expands down and to the right. ```swift try engine.block.scale(block, to: 1.5) ``` ![Original image and scaled image](../mobile-assets/scale-example-3.png) By default, the anchor point for the video when scaling is the origin point on the top left. The scale function has two optional parameters to move the anchor point in the x and y direction. They can have values between `0.0` and `1.0` This scales the video to 150% of its original size. The origin anchor point is 0.5, 0.5 so the video expands from the center. ```swift try engine.block.scale(block, to: 1.5, anchorX: 0.5, anchorY: 0.5) ``` ![Original video placed over the scaled video, aligned on the center anchor point](../mobile-assets/scale-example-4.png) *** ### Scale non-uniformly To stretch or compress only one axis, thus distorting a video, use the crop scale function in combination with the width or height function. How you decide to make the adjustment will have different results. Below are three examples of scaling the original video in the x direction only. ![Allowing the engine to scale the video as you adjust the width of the block](../mobile-assets/scale-example-5.png) ```swift try engine.block.setWidthMode(imageBlock, mode: .absolute) let newWidth: Float = try engine.block.getWidth(imageBlock) * 1.5 try engine.block.setWidth(imageBlock, value: newWidth) ``` This adjusts the width of the block and allows the engine to adjust the scale of the video to maintain it as a fill. The video isn't distorted, but it no longer fits the frame of the block. ![Using crop scale for the horizontal axis and adjusting the width of the block](../mobile-assets/scale-example-6.png) ```swift try engine.block.setCropScaleX(block, scaleX: 1.50) try engine.block.setWidthMode(block, mode: .absolute) let newWidth: Float = try engine.block.getWidth(block) * 1.5 try engine.block.setWidth(block, value: newWidth) ``` This uses crop scale to scale the video in a single direction and then adjusts the block's width to match the change. The change in width does not take the crop into account and so distorts the video as it's scaling the scaled video. ![Using crop scale for the horizontal axis and using the maintainCrop property when changing the width](../mobile-assets/scale-example-7.png) ```swift try engine.block.setCropScaleX(block, scaleX: 1.50) try engine.block.setWidthMode(block, mode: .absolute) let newWidth: Float = try engine.block.getWidth(block) * 1.5 try engine.block.setWidth(block, value: newWidth, maintainCrop: true) ``` By setting the `maintainCrop` option to true, expanding the width of the video by the scale factor respects the crop scale and the video is less distorted. ## Scale multiple elements together Group blocks to scale them proportionally: ```swift let groupId = try engine.block.group([videoBlock, textBlock]) try engine.block.scale(groupId, to: 0.75) ``` This scales the entire group to 75%. *** ## Lock scaling A standard pinch-to-zoom gesture allows a user to scale a block. Toggle this ability for users by changing the "touch/pinchAction" property of the `editor`: ```swift //disable pinch-to-scale try engine.editor.setSettingEnum("touch/pinchAction", value: "None") ``` By default, video clip blocks in the **Video Editor** do not enable their scale handles, toggle this ability using the `controlGizmo/showScaleHandles` property of the `editor`. Displaying the scale handles will allow the user to scale even when pinch-to-zoom is disabled. ```swift //show scale handles try engine.editor.setSettingBool("controlGizmo/showScaleHandles", value: true) ``` ![Video clip with scale handles enabled in Video Editor](../mobile-assets/scale-example-1.png) > **Note:** When working with an editor such as the **Video Editor**, editor settings are > best set in the `EditorConfiguration.onCreate` callback. When working directly with the > **engine** they can be set at any time. When working with templates, you can lock a block from scaling by setting its scope. Remember that the global layer has to defer to the blocks using `setGlobalScope`. ```swift try engine.block.setScopeEnabled(videoBlock, key: "layer/resize", enabled: false) ``` To prevent users from transforming an element at all: ```swift try engine.block.setTransformLocked(videoBlock, locked: true) ``` *** --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Trim" description: "Learn how to trim video and audio clips in CE.SDK for Swift by setting trim offsets and trim lengths with the Engine API." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Create and Edit Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) > [Trim](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-video-trim/Trim.swift reference-only import Foundation import IMGLYEngine @MainActor func trim(engine: Engine) async throws { let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 60) let baseURL = try engine.guidesBaseURL let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) func makeVideoBlock() async throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.video) try engine.block.setURL(fill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(block, fill: fill) try engine.block.appendChild(to: page, child: block) try await engine.block.forceLoadAVResource(fill) try engine.block.setDuration(block, duration: 10) return block } let videoBlock = try await makeVideoBlock() let videoFill = try engine.block.getFill(videoBlock) let sourceDuration = try engine.block.getAVResourceTotalDuration(videoFill) print("Source media duration: \(sourceDuration)s") let canTrim = try engine.block.supportsTrim(videoFill) print("Video fill supports trimming: \(canTrim)") try engine.block.setTrimOffset(videoFill, offset: 2.0) try engine.block.setTrimLength(videoFill, length: 5.0) let trimOffset = try engine.block.getTrimOffset(videoFill) let trimLength = try engine.block.getTrimLength(videoFill) print("Playing \(trimLength)s starting at \(trimOffset)s into the source") let durationBlock = try await makeVideoBlock() let durationFill = try engine.block.getFill(durationBlock) try engine.block.setLooping(durationFill, looping: false) try engine.block.setTrimOffset(durationFill, offset: 3.0) try engine.block.setTrimLength(durationFill, length: 5.0) if try engine.block.supportsDuration(durationBlock) { try engine.block.setDuration(durationBlock, duration: 5.0) } let loopBlock = try await makeVideoBlock() let loopFill = try engine.block.getFill(loopBlock) try engine.block.setLooping(loopFill, looping: true) try engine.block.setTrimOffset(loopFill, offset: 5.0) try engine.block.setTrimLength(loopFill, length: 3.0) try engine.block.setDuration(loopBlock, duration: 9.0) print("Looping enabled: \(try engine.block.isLooping(loopFill))") let frameBlock = try await makeVideoBlock() let frameFill = try engine.block.getFill(frameBlock) // Supply the frame rate from your media pipeline; iOS does not expose // source frame rate through the Engine API. let knownFrameRate = 30.0 let startFrame = 60 let frameCount = 150 try engine.block.setTrimOffset(frameFill, offset: Double(startFrame) / knownFrameRate) try engine.block.setTrimLength(frameFill, length: Double(frameCount) / knownFrameRate) let trimmableFills = try engine.block.find(byType: .graphic) .map { try engine.block.getFill($0) } .filter { try engine.block.supportsTrim($0) } for fill in trimmableFills { try await engine.block.forceLoadAVResource(fill) if try engine.block.getAVResourceTotalDuration(fill) >= 4.0 { try engine.block.setTrimOffset(fill, offset: 1.0) try engine.block.setTrimLength(fill, length: 3.0) } } let audioBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: audioBlock) let audioURL = baseURL.appendingPathComponent("ly.img.audio/audios/far_from_home.m4a") try engine.block.setURL(audioBlock, property: "audio/fileURI", value: audioURL) try await engine.block.forceLoadAVResource(audioBlock) try engine.block.setTrimOffset(audioBlock, offset: 1.0) try engine.block.setTrimLength(audioBlock, length: 8.0) try engine.block.setTimeOffset(audioBlock, offset: 2.0) try engine.block.setDuration(audioBlock, duration: 8.0) try engine.block.setVolume(audioBlock, volume: 0.7) } ``` Control which part of a video or audio source plays by setting trim offsets and trim lengths, while leaving the original media file unchanged. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-video-trim) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Trimming works together with block timing. Fill-level trim values choose the portion of the source media that plays, while block-level timing controls when that block appears in the composition and how long it stays active. This guide focuses on the Engine APIs for trimming video fills and audio blocks programmatically. For UI-based timeline editing, see the [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) guide. ## Setting Up the Scene Create a video scene, add a page, and give the page a fixed size and total duration so the trim demos below run against a stable timeline. ```swift highlight-trim-setupScene let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 60) ``` ## Creating a Video Block The `makeVideoBlock` helper creates a `.graphic` block with a rectangular shape and a video fill, appends it to the page, and assigns a 10-second block duration. The helper awaits `forceLoadAVResource(_:)` before returning—loading the media is mandatory because trim and duration properties rely on the underlying clip's duration metadata. ```swift highlight-trim-makeVideoBlock let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) func makeVideoBlock() async throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.video) try engine.block.setURL(fill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(block, fill: fill) try engine.block.appendChild(to: page, child: block) try await engine.block.forceLoadAVResource(fill) try engine.block.setDuration(block, duration: 10) return block } ``` ## Understanding Trim Concepts ### Fill-Level Trimming Fill-level trimming controls the source media range. Use `setTrimOffset(_:offset:)` to choose where playback starts inside the media file and `setTrimLength(_:length:)` to choose how much media plays from that point. A trim offset of `2.0` skips the first two seconds of the source. A trim length of `5.0` then plays five seconds from that offset, so the visible range runs from second 2 to second 7 of the original media. Trimming is non-destructive—the source file stays unchanged, so you can adjust the values at any time to show a different portion of the same media. ### Block-Level Timing Block-level timing controls placement in the composition. `setTimeOffset(_:offset:)` moves a block relative to its parent timeline, while `setDuration(_:duration:)` controls how long the block remains active. Use trim values for source-media in and out points. Use time offset when you need to arrange clips or audio blocks in the composition. For non-looping video fills, changing the trim length updates the connected block duration; treat `setDuration(_:duration:)` as timeline timing and set fill trim values explicitly when you change source-media in or out points. ### Common Use Cases - **Remove unwanted segments** - Skip intro or outro sections without changing the source file. - **Extract key moments** - Use a short range from a longer video or audio clip. - **Sync audio and video** - Trim related media independently while keeping their timeline offsets aligned. - **Create loops** - Trim a short segment and repeat it through the block duration. ## Programmatic Video Trimming ### Checking Source Duration Read the total duration of the loaded source with `getAVResourceTotalDuration(_:)`. Use it to validate that a requested trim range fits within the available media before applying it. ```swift highlight-trim-sourceDuration let videoBlock = try await makeVideoBlock() let videoFill = try engine.block.getFill(videoBlock) let sourceDuration = try engine.block.getAVResourceTotalDuration(videoFill) print("Source media duration: \(sourceDuration)s") ``` ### Checking Trim Support Check trim support before applying trim operations. Video fills and audio blocks support trimming, but pages, scenes, and many graphic-only blocks do not. This check keeps custom editing UI from offering trim controls on unsupported blocks. ```swift highlight-trim-checkSupport let canTrim = try engine.block.supportsTrim(videoFill) print("Video fill supports trimming: \(canTrim)") ``` ### Trimming Video Apply the trim offset and trim length to the video fill. The values are seconds in the media timeline and are scaled by playback rate. ```swift highlight-trim-applyTrim try engine.block.setTrimOffset(videoFill, offset: 2.0) try engine.block.setTrimLength(videoFill, length: 5.0) ``` This example skips the first two seconds and plays the following five seconds. ### Getting Current Trim Values Read trim values when you need to populate UI controls, verify a change, or make relative adjustments. The getters return seconds, the same unit the setters take. ```swift highlight-trim-readTrimValues let trimOffset = try engine.block.getTrimOffset(videoFill) let trimLength = try engine.block.getTrimLength(videoFill) print("Playing \(trimLength)s starting at \(trimOffset)s into the source") ``` ## Additional Trimming Techniques ### Trimming with Block Duration Trim length and block duration work together, but they are not interchangeable. For non-looping video fills, call `setTrimLength(_:length:)` on the fill to choose the source segment; the Engine updates the connected block duration from that trim length. Use `setDuration(_:duration:)` on the block when you need to control how long the block stays active in the composition. ```swift highlight-trim-withDuration let durationBlock = try await makeVideoBlock() let durationFill = try engine.block.getFill(durationBlock) try engine.block.setLooping(durationFill, looping: false) try engine.block.setTrimOffset(durationFill, offset: 3.0) try engine.block.setTrimLength(durationFill, length: 5.0) if try engine.block.supportsDuration(durationBlock) { try engine.block.setDuration(durationBlock, duration: 5.0) } ``` Here the trim length and block duration are both five seconds, so the trimmed segment plays once. To keep a block duration longer than the trim length, enable looping before setting the longer duration. ### Trimming with Looping Enable looping when a trimmed segment should repeat until the block duration is filled. Check the current looping state with `isLooping(_:)`. ```swift highlight-trim-withLooping let loopBlock = try await makeVideoBlock() let loopFill = try engine.block.getFill(loopBlock) try engine.block.setLooping(loopFill, looping: true) try engine.block.setTrimOffset(loopFill, offset: 5.0) try engine.block.setTrimLength(loopFill, length: 3.0) try engine.block.setDuration(loopBlock, duration: 9.0) print("Looping enabled: \(try engine.block.isLooping(loopFill))") ``` Here the three-second trimmed segment repeats to fill the nine-second block duration. Without looping, playback stops when the trim length is reached and the block holds the last frame for the remaining duration. ### Frame-Accurate Trimming When your app works from known frame numbers, convert those frame values to seconds before setting the trim APIs. ```swift highlight-trim-frameAccurate let frameBlock = try await makeVideoBlock() let frameFill = try engine.block.getFill(frameBlock) // Supply the frame rate from your media pipeline; iOS does not expose // source frame rate through the Engine API. let knownFrameRate = 30.0 let startFrame = 60 let frameCount = 150 try engine.block.setTrimOffset(frameFill, offset: Double(startFrame) / knownFrameRate) try engine.block.setTrimLength(frameFill, length: Double(frameCount) / knownFrameRate) ``` The trim APIs accept seconds. Keep the frame rate value tied to the source media you are trimming. ### Batch Processing Multiple Videos For repeated trim settings, collect the trimmable video fills, load each resource, and apply the same range to every compatible fill once the durations are known. Always load each fill before reading its duration—source media can have different lengths. ```swift highlight-trim-batchVideos let trimmableFills = try engine.block.find(byType: .graphic) .map { try engine.block.getFill($0) } .filter { try engine.block.supportsTrim($0) } for fill in trimmableFills { try await engine.block.forceLoadAVResource(fill) if try engine.block.getAVResourceTotalDuration(fill) >= 4.0 { try engine.block.setTrimOffset(fill, offset: 1.0) try engine.block.setTrimLength(fill, length: 3.0) } } ``` ### Trimming Audio Blocks Audio blocks use the same trim APIs as video fills. After loading the audio resource, set its trim range, timeline placement, and active duration separately. ```swift highlight-trim-audio let audioBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: audioBlock) let audioURL = baseURL.appendingPathComponent("ly.img.audio/audios/far_from_home.m4a") try engine.block.setURL(audioBlock, property: "audio/fileURI", value: audioURL) try await engine.block.forceLoadAVResource(audioBlock) try engine.block.setTrimOffset(audioBlock, offset: 1.0) try engine.block.setTrimLength(audioBlock, length: 8.0) try engine.block.setTimeOffset(audioBlock, offset: 2.0) try engine.block.setDuration(audioBlock, duration: 8.0) try engine.block.setVolume(audioBlock, volume: 0.7) ``` Use `setTimeOffset(_:offset:)` when the audio should start later in the composition. Match `setDuration(_:duration:)` to the trim length when the selected audio range should play once, and use `setVolume(_:volume:)` when the trimmed clip needs a different level. ## Troubleshooting | Issue | Fix | | --- | --- | | Trim values have no visible effect | Await `forceLoadAVResource(_:)` before setting or reading trim properties. | | Trim starts at the wrong point | Use `setTrimOffset(_:offset:)` for the source-media start point and `setTimeOffset(_:offset:)` for timeline placement. | | Playback continues longer than expected | Check whether looping is enabled, then read back `getDuration(_:)` and `getTrimLength(_:)` after changing trim or duration controls. | | Audio and video drift out of sync | Apply coordinated trim offsets and timeline offsets to both media blocks. | ## API Reference ### Methods | Method | Description | | --- | --- | | `find(byType:)` | Find all blocks of a `DesignBlockType` | | `getFill(_:)` | Get the fill block attached to a graphic block | | `setURL(_:property:value:)` | Set a URI property such as the video fill source (`fill/video/fileURI`) or audio source (`audio/fileURI`) | | `forceLoadAVResource(_:)` | Load audio or video metadata before trim and duration access | | `getAVResourceTotalDuration(_:)` | Return the source media duration in seconds | | `supportsTrim(_:)` | Check whether a block or fill supports trim properties | | `setTrimOffset(_:offset:)` | Set the source-media playback start in seconds | | `getTrimOffset(_:)` | Read the current trim offset in seconds | | `setTrimLength(_:length:)` | Set how much source media plays from the trim offset | | `getTrimLength(_:)` | Read the current trim length in seconds | | `supportsDuration(_:)` | Check whether a block supports playback duration | | `setDuration(_:duration:)` | Set how long the block is active in the composition | | `getDuration(_:)` | Read the block duration in seconds | | `setTimeOffset(_:offset:)` | Set when the block becomes active in its parent timeline | | `setLooping(_:looping:)` | Enable or disable looping for the media block or fill | | `isLooping(_:)` | Read whether looping is enabled | | `setVolume(_:volume:)` | Set audio volume from `0.0` to `1.0` | ## Next Steps - [Split Video and Audio](https://img.ly/docs/cesdk/mac-catalyst/edit-video/split-464167/) - Split video and audio clips at specific time points, creating two independent segments from a single clip. - [Control Audio and Video](https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/) - Master playback controls, volume, and muting. - [Video Timeline Overview](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) - Understand the complete timeline editing model. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Engine Interface" description: "Understand CE.SDK's architecture and learn when to use direct Engine access for automation workflows" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Engine](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) --- ```swift file=@cesdk_swift_examples/engine-guides-engine-interface/EngineInterface.swift reference-only import IMGLYEngine @MainActor func engineInterface(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) let json = try await engine.scene.saveToString() try await engine.scene.load(from: json) } @MainActor func makeOffscreenEngine(license: String) async throws -> Engine { try await Engine( context: .offscreen(size: .init(width: 1024, height: 1024)), audioContext: .none, license: license, ) } ``` The CE.SDK Engine is the C++-powered core behind every creative operation. The Engine interface gives you direct programmatic control across iOS, macOS, and Mac Catalyst — from validation and thumbnail generation to background exports and batch automation. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-engine-interface) CE.SDK is built on a layered architecture: a cross-platform C++ engine handles rendering, scene management, and creative operations, and Swift bindings expose that surface to your app through `IMGLYEngine`. On iOS, the prebuilt editor UI calls the same bindings under the hood. Whether you reach for the editor or the engine directly, you get identical capabilities. ## Swift SDK Modules CE.SDK ships several Swift modules. Pick the smallest one that matches your integration: | Module | Availability | Use it for | |---|---|---| | `IMGLYEngine` | iOS 14+, macOS 12+, Mac Catalyst | Engine-only access. Initialize directly with `try await Engine(...)`. The foundation for headless work, custom editors, and hidden Engine instances. | | `IMGLYEditor` | iOS 16+ | The prebuilt SwiftUI editor. Compose a Starter Kit configuration (Design, Video, Photo, Apparel, Postcard) or assemble your own with `EditorConfiguration`. | | `IMGLYCamera` | iOS 16+ | The prebuilt camera UI. Returns recordings ready to feed into the editor. | `IMGLYEngine` is the only module that ships on macOS and Mac Catalyst — the editor and camera modules build on iOS only. On those non-iOS targets, initialize `IMGLYEngine` directly and drive the engine from your own AppKit or SwiftUI surface. ## Engine API Namespaces The Engine organizes its functionality into six namespaces. Each groups methods for one domain — content hierarchy, design elements, assets, editor settings, template variables, and reactive updates: ```swift highlight-engineInterface-namespaces @MainActor func engineInterface(engine: Engine) async throws { let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) let json = try await engine.scene.saveToString() try await engine.scene.load(from: json) } ``` | Namespace | Purpose | |---|---| | `engine.scene` | Create, load, and save scenes and pages. | | `engine.block` | Create, modify, and export design elements (graphics, text, audio, video). | | `engine.asset` | Register and query asset sources (images, templates, fonts). | | `engine.editor` | Configure editor settings, manage edit modes, handle undo and redo. | | `engine.variable` | Define and update template variables for data merge. | | `engine.event` | Subscribe to engine events such as selection changes and block updates. | ## Combining UI and Engine Access On iOS, the prebuilt editor UI calls the same engine APIs internally. Reach the Engine through your editor configuration's `onCreate` callback — a `@MainActor` closure that receives the started Engine and runs before the editor renders. Use it for validation, scene preloading, custom asset sources, or any other engine-level work that should happen alongside visual editing. On macOS and Mac Catalyst, the prebuilt editor UI is not available. Initialize the Engine directly with `try await Engine(...)` and build your own surface on top of the same namespaces. ## Hidden Engine Instances Each `Engine` is an independent runtime. You can keep one Engine driving the editor while a second instance does work in the background — design validation, thumbnail generation, or preview rendering — without affecting what the user sees. To run an Engine without a visible view, initialize it with an offscreen render context: ```swift highlight-engineInterface-offscreen @MainActor func makeOffscreenEngine(license: String) async throws -> Engine { try await Engine( context: .offscreen(size: .init(width: 1024, height: 1024)), audioContext: .none, license: license, ) } ``` The offscreen instance has full access to the same six API namespaces — load scenes, run blocks through the export pipeline, and drop the reference when the work completes. ## Memory Management The Swift `Engine` is reference-counted. ARC tears it down and releases its GPU resources when the last strong reference goes out of scope. Hold one Engine for the lifetime of an editing session and let ARC release it on scope exit — there is no explicit `dispose()` call to make. For background processing where you spin up an additional Engine instance, drop the reference as soon as the work completes. The instance will deallocate and reclaim its buffers. ## Choosing an Approach | Scenario | Approach | |---|---| | Interactive end-user editing | For iOS, the prebuilt editor UI (`IMGLYEditor`) with a Starter Kit configuration. On macOS and Mac Catalyst, a custom surface built on `IMGLYEngine`. | | Background validation or thumbnail generation | A hidden `IMGLYEngine` instance. | | Custom Apple-native UI with full engine control | `IMGLYEngine` plus your own SwiftUI or AppKit view. | | High-resolution or bulk server-side export | Offload to a Node.js backend running `@cesdk/node`. | ## Troubleshooting **`try await Engine(...)` throws on initialization.** Confirm the CE.SDK license key passed to `license:` is valid and that your bundle identifier matches the one registered with the license. Some licenses also require a non-empty `userID:`. **Engine APIs report a `MainActor` warning.** The `Engine` class is `@MainActor`-isolated. Call its methods from a `@MainActor` context — either a `@MainActor` function, a SwiftUI view body, or an explicit `Task { @MainActor in ... }`. **A hidden Engine instance keeps memory pinned.** Drop the strong reference when the offscreen work completes. ARC releases the engine and reclaims its GPU buffers once no caller is holding it. ## API Reference | API | Purpose | |---|---| | `Engine(context:audioContext:license:userID:)` | Initialize a standalone Engine. Async; throws on invalid license. Defaults `context: .metal` (renders to a Metal view); pass `.offscreen(size:)` for headless work. | | `engine.scene.create()` | Create an empty scene. | | `engine.scene.saveToString()` | Serialize the current scene to a string. | | `engine.scene.load(from:)` | Load a scene from a serialized string, or a scene or archive `.imgly` file from a URL. | | `engine.block.create(_:)` | Create a new block of a given type. | | `engine.block.appendChild(to:child:)` | Add a block as a child of a parent. | ## Next Steps - [Architecture](https://img.ly/docs/cesdk/mac-catalyst/concepts/architecture-6ea9b2/) — Explore the Engine and its six API namespaces in depth. - [Batch Processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/) — Process multiple designs in one flow. - [Data Merge](https://img.ly/docs/cesdk/mac-catalyst/automation/data-merge-ae087c/) — Personalize templates with external data. - [Node.js SDK](#broken-link-n1234a) — Use the server-side Engine package for backend processing. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Equal-Distance Snapping" description: "Snap a dragged block to equal spacing with its siblings and show the size of each gap." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/equal-distance-snapping-e9d15a/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). --- Turn on equal-distance snapping so a dragged block lands at an equal distance from its siblings, and a badge shows the size of each gap. > **Reading time:** 4 minutes While the user drags a block, the engine looks for sibling blocks that already sit at an equal distance from each other. When the dragged block comes close to a position that continues or splits that spacing, the engine pulls the block onto it. It then draws a line across each gap, a tick at both ends of the line, and a badge with the size of the gap. The engine draws these indicators, so iOS, Android and Web all show the same thing. There is no iOS-only view to add. ## Turn the Feature On Equal-distance snapping is off by default. Turn it on per engine instance with the `features/equalDistanceSnappingEnabled` setting. ```swift try engine.editor.setSettingBool( "features/equalDistanceSnappingEnabled", value: true ) ``` Call this on the main thread, like every other engine call. A good place is the `onCreate` callback of your editor configuration, before the scene loads. ```swift static let preCreateScene: OnCreate.Callback = { engine in try engine.editor.setSettingBool( "features/equalDistanceSnappingEnabled", value: true ) } ``` The setting takes effect on the next drag. You can turn it off again at any time. ## Understand When the Snap Fires The engine measures gaps along one axis and needs the blocks to overlap across that axis. Two blocks in the same row form one horizontal gap. Two blocks in the same column form one vertical gap. The dragged block must overlap each block of the chain across that axis, and each pair of neighbours must overlap each other. Both axes are checked on every drag, so a block can snap on X and on Y at the same time. From one chain of equal gaps, the engine offers three positions: - **After the chain.** The dragged block continues the spacing on the right, or below. - **Before the chain.** The dragged block continues the spacing on the left, or above. - **Inside a single gap.** The dragged block splits one gap into two equal halves. This needs the block to fit in the gap. Only blocks on the same page take part. A block that overlaps its neighbour has no gap with it, so the chain stops there. ## Control the Pull Distance `positionSnappingThreshold` sets how close the drag must come before the block is pulled. Edge and center snapping share the same value. It is in screen points, so the pull feels the same at any zoom. ```swift try engine.editor.setSettingFloat("positionSnappingThreshold", value: 4) ``` ## Style the Indicators `snappingGuideColor` sets the color of the measurement line, of the ticks, and of the badge background. `handleFillColor` sets the color of the label inside the badge, so pick a color with a strong contrast against the background. ```swift try engine.editor.setSettingColor( "snappingGuideColor", r: 1, g: 0.004, b: 0.361, a: 1 ) ``` The badge measures its own label, so it always fits the number. Every gap of a chain shows the same number: the spacing the snap holds, in design units, to one decimal. A badge wider than its gap moves above or beside the line. ## Combine With Other Snapping Equal-distance snapping runs first on each axis. When an equal-distance position is in reach, it takes that axis, even when an edge or center guide sits nearer. The engine uses the edge and center guides only when no equal-distance position is in reach. The badge appears only on an axis that equal distance won. While an equal-distance snap holds the block, the engine does not round the position to the pixel grid. Rounding would break the equality that the badges show. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Export Counting" description: "Learn which operations count as an export in CE.SDK, when export events are recorded, and what data they contain." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-counting-613923/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Concepts](https://img.ly/docs/cesdk/mac-catalyst/concepts-c9ff51/) > [Export Counting](https://img.ly/docs/cesdk/mac-catalyst/export-counting-613923/) --- Some CE.SDK plans use usage-based pricing that meters exports. This page defines exactly which operations count as an export, when an export is recorded, and which data is collected along with it. For an overview of licensing options, see [Licensing](https://img.ly/docs/cesdk/mac-catalyst/licensing-8aa063/). For a broader look at data collection and privacy, see [Security](https://img.ly/docs/cesdk/mac-catalyst/security-777bfd/). ## What Counts as an Export CE.SDK records one export event per call to an export API. This applies to: - Exporting a block to an image format such as PNG, JPEG, WebP or TGA - Exporting to PDF or SVG - Exporting raw pixel data through the export API - Exporting with a color mask - Exporting a video - Exporting audio - Exporting a page as HTML with the HTML exporter Each call produces exactly one event, regardless of the content being exported. The engine doesn't distinguish between a "final" export and any other use of the export API. For example, if you call the export API to generate a thumbnail of a design programmatically, that call counts as an export like any other. To show a design in your app without counting an export, use a [preview](#previews) instead. ## What Doesn't Count The following operations never record an export event: - **Live rendering in the editor.** The canvas preview your users see while editing isn't an export. - **Saving scenes.** Saving a scene to a string or archive—including autosaves and drafts—isn't an export. Only the export APIs count. - **Built-in thumbnail APIs.** The engine's thumbnail generation APIs, such as video and audio thumbnail sequences and the page thumbnails shown in the editor UI, don't go through the export pipeline and aren't counted. - **Previews.** An image from `exportPreview` isn't an export. See [Previews](#previews). ## When an Export Is Counted An export is counted after the engine finishes it successfully. A failed or canceled export—for example due to an invalid block or an encoding error—isn't counted. This applies to images, PDFs, videos and audio alike. On the web, an image or PDF export that the engine returns in memory counts as soon as the engine has encoded it. If you cancel the export after that point, it still counts. A large PDF, a video or an audio file is first written to the browser's file storage, and counts only once your code receives the file. If the browser fails to hand that file over to you, the export isn't counted. A PDF that you receive in chunks while it is exported counts only once the whole document has reached you. A streamed export that stops early isn't counted, because the pages you received don't form a PDF that opens. An export is counted when the export API call executes—not when a file is downloaded, uploaded or attached to a business event. There is no built-in option to defer counting to a later point such as a checkout. If you want exports to align with a business event, structure your integration so the export API is only called at that point. ## Previews `exportPreview` gives you a small image of a block to show in your app, for example in a design picker or a cart. It's available as `engine.block.exportPreview(block)` on Web and Node.js, and as `exportPreview` on iOS and Android. A preview doesn't count as an export. A preview is limited so that it can't replace an export: - It's always a still WebP image at quality 0.8. For a video, it shows one frame. To show frames across a video, use the video thumbnail sequence API, which doesn't count either. - It has at most 262,144 pixels, which is 512 × 512. A larger block is scaled down to fit and keeps its aspect ratio. A smaller block keeps its export size. Use the export API for any file that you deliver. A preview sends a preview event with its width and height, so we can see how previews are used, but the event isn't billed. A preview can fail while a video export runs, because the engine renders one export at a time. Wait until the video export finishes, then request the preview again. ## HTML Exports The HTML exporter renders some blocks as images, for example a block with an effect or a shape that HTML can't express. One call to `exportHtml` counts as one export of the page. Each block it renders still sends an export event, marked as part of that HTML export, so it shows up in your usage but isn't counted. With a CE.SDK version before 1.86, each rendered block counts as an image export of its own. Update CE.SDK to count one export per page. ## Multi-Page Documents - **PDF:** Exporting multiple pages in a single call produces one PDF file and one export event. The event includes the number of pages exported and the dimensions of the largest page. - **Image formats:** Each page requires its own export call, so exporting a multi-page document as images produces one event per page. ## Multiple Formats Each export call is counted separately. Exporting the same design once as a PDF and once as a PNG produces two export events, one per format. ## Development, Staging and Production CE.SDK doesn't distinguish between environments. A license key can be used across development, staging and production, and exports are counted the same way in all of them. ## Server-Side Exports and Renderer The same counting rules apply on every platform. Exports performed with the Node.js SDK or other server-side integrations are counted under the same definition as client-side exports. The [CE.SDK Renderer](#broken-link-7f3e9a) uses the same export counting and additionally sends periodic heartbeats to track the number of active instances, as described in the [Security](https://img.ly/docs/cesdk/mac-catalyst/security-777bfd/) documentation. ## What Data Is Collected An export event contains only technical metadata—never your content. Image and PDF export events include the media type, format, resolution, page count and megapixels. Video export events include the media type, format, resolution, megapixels of one frame, frame rate and duration. Audio export events include the media type, format and duration. HTML export events include the resolution, page count and megapixels of the exported page. Every export event names the package that produced it, such as the engine or the HTML exporter, and includes the size of the file in bytes where the engine knows it. An export event also carries the custom export metadata that your integration sets. Events are associated with the user ID (if provided by your integration), device ID and session ID described in the [Security](https://img.ly/docs/cesdk/mac-catalyst/security-777bfd/) documentation. A video, audio or HTML export that fails or is canceled sends a failure event with the type, format and reason. Failure events aren't counted. The user ID is transmitted exactly as your integration provides it and is used solely to deduplicate users when counting monthly active users. It doesn't need to be a real identifier: if you want to keep your internal user IDs private, pass a hashed or otherwise opaque value instead—deduplication works just as well, as long as the value is unique and stable per user. Export events are only sent when tracking is enabled for your license. Enterprise licenses with offline validation can opt out of tracking entirely; [contact our sales team](https://img.ly/forms/contact-sales/) to explore these options. ## Export Counts and Billing Export events are the technical metering primitive. How those counts map to your bill—which tiers apply and how usage is aggregated—is defined by your plan and contract, not by the SDK. If you have questions about how exports are billed under your agreement, [contact our sales team](https://img.ly/forms/contact-sales/). --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Create Thumbnail" description: "Generate thumbnail preview images from CE.SDK scenes by exporting with target dimensions for galleries and design management." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/create-thumbnail-749be1/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [Create Thumbnail](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/create-thumbnail-749be1/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-thumbnail/CreateThumbnail.swift reference-only import Foundation import IMGLYEngine @MainActor func createThumbnail(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let outputDirectory = FileManager.default.temporaryDirectory // Demo scaffolding: build a design (a photo with a title banner) so the // exported thumbnails preview real content. In your app you start from a // scene the user is already editing. let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") try await engine.scene.create(fromImage: imageURL) guard let designPage = try engine.scene.getCurrentPage() else { fatalError("Expected create(fromImage:) to create a page.") } // A design scene from an image keeps font sizes in points; pin them to pixels // so the title below is sized in the same unit as the page dimensions. try engine.scene.setFontSizeUnit(.px) let designWidth = try engine.block.getWidth(designPage) let designHeight = try engine.block.getHeight(designPage) let banner = try engine.block.create(.graphic) try engine.block.setShape(banner, shape: engine.block.createShape(.rect)) let bannerFill = try engine.block.createFill(.color) try engine.block.setColor(bannerFill, property: "fill/color/value", color: .rgba(r: 0.05, g: 0.09, b: 0.16, a: 0.72)) try engine.block.setFill(banner, fill: bannerFill) try engine.block.setWidth(banner, value: designWidth) try engine.block.setHeight(banner, value: designHeight * 0.22) try engine.block.setPositionX(banner, value: 0) try engine.block.setPositionY(banner, value: designHeight * 0.78) try engine.block.appendChild(to: designPage, child: banner) let title = try engine.block.create(.text) try engine.block.replaceText(title, text: "Coastal Escape") try engine.block.setTextColor(title, color: .rgba(r: 1, g: 1, b: 1, a: 1)) try engine.block.setTextFontSize(title, fontSize: designHeight * 0.09) try engine.block.setHeightMode(title, mode: .auto) try engine.block.setWidth(title, value: designWidth * 0.86) try engine.block.setPositionX(title, value: designWidth * 0.07) try engine.block.setPositionY(title, value: designHeight * 0.84) try engine.block.appendChild(to: designPage, child: title) try await engine.captureGuide(designPage, label: "hero") let page = try engine.scene.getCurrentPage() ?? engine.scene.getPages().first guard let page else { fatalError("Load a scene with at least one page before exporting a thumbnail.") } let thumbnail = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(targetWidth: 400, targetHeight: 300), ) print("Thumbnail: \(thumbnail.count) bytes") let jpegThumbnail = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.8, targetWidth: 400, targetHeight: 300), ) print("JPEG thumbnail: \(jpegThumbnail.count) bytes") let pngThumbnail = try await engine.block.export( page, mimeType: .png, options: ExportOptions(pngCompressionLevel: 6, targetWidth: 400, targetHeight: 300), ) print("PNG thumbnail: \(pngThumbnail.count) bytes") let webpThumbnail = try await engine.block.export( page, mimeType: .webp, options: ExportOptions(webpQuality: 0.85, targetWidth: 400, targetHeight: 300), ) print("WebP thumbnail: \(webpThumbnail.count) bytes") let preview = try await engine.block.exportPreview(page) print("Preview: \(preview.count) bytes") let targetSizes: [(width: Float, height: Float)] = [(150, 150), (400, 300), (800, 600)] var responsiveSet: [Data] = [] for size in targetSizes { let sized = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.8, targetWidth: size.width, targetHeight: size.height), ) responsiveSet.append(sized) print("\(Int(size.width))x\(Int(size.height)): \(sized.count) bytes") } let savedThumbnail = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.8, targetWidth: 400, targetHeight: 300), ) let fileURL = outputDirectory.appendingPathComponent("thumbnail.jpg") try savedThumbnail.write(to: fileURL) let videoScene = try engine.scene.createVideo() let videoPage = try engine.block.create(.page) try engine.block.appendChild(to: videoScene, child: videoPage) try engine.block.setWidth(videoPage, value: 1280) try engine.block.setHeight(videoPage, value: 720) try engine.block.setDuration(videoPage, duration: 10) let clip = try engine.block.create(.graphic) try engine.block.setShape(clip, shape: engine.block.createShape(.rect)) let clipFill = try engine.block.createFill(.video) try engine.block.setURL( clipFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) try engine.block.setFill(clip, fill: clipFill) try engine.block.setWidth(clip, value: 1280) try engine.block.setHeight(clip, value: 720) try engine.block.appendChild(to: videoPage, child: clip) try await engine.block.forceLoadAVResource(clipFill) if try engine.block.supportsPlaybackTime(videoPage) { try engine.block.setPlaybackTime(videoPage, time: 2.0) } let videoThumbnail = try await engine.block.export( videoPage, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.8, targetWidth: 400, targetHeight: 225), ) try videoThumbnail.write(to: outputDirectory.appendingPathComponent("video-thumbnail.jpg")) } ``` Generate small preview images from CE.SDK scenes for galleries, file browsers, and design management interfaces by exporting a page with target dimensions. ![An aerial coastal photo with a dark title banner and a white "Coastal Escape" heading — the source design that the thumbnails preview.](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-create-thumbnail) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. Thumbnails use the same block export API as full-size image exports. Pass a page to `engine.block.export(_:mimeType:options:)`, choose an image `MIMEType`, and set target dimensions in `ExportOptions`. The call returns the image as `Data` that you can decode, cache, write to disk, or upload. This guide focuses on static image thumbnails produced by the export API. For multi-frame scrubber previews, page storyboards, and audio waveforms, see [Thumbnail Previews](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/thumbnail-previews-dcf031/), which streams each frame and sample chunk as it becomes ready. ## Export a Thumbnail Start from the current page so the thumbnail matches the visible canvas. When no page is selected, fall back to the first page in the loaded scene. Export the page block rather than an individual element to capture the full composited design. ```swift highlight-createThumbnail-selectPage let page = try engine.scene.getCurrentPage() ?? engine.scene.getPages().first guard let page else { fatalError("Load a scene with at least one page before exporting a thumbnail.") } ``` Call `engine.block.export(_:mimeType:options:)` with `targetWidth` and `targetHeight` set together. CE.SDK renders the page large enough to fill the target box while preserving its aspect ratio, so the output may extend beyond one axis when the source aspect ratio differs. ```swift highlight-createThumbnail-export let thumbnail = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(targetWidth: 400, targetHeight: 300), ) print("Thumbnail: \(thumbnail.count) bytes") ``` The returned `Data` is raw image bytes in the requested format — nothing is written to disk until you do so yourself. ## Choose a Thumbnail Format Select the format with the `mimeType` argument, using the `MIMEType` case that fits the UI surface where you display the thumbnail: - **`.jpeg`** — Smaller files for photographic content, tuned with `jpegQuality`. - **`.png`** — Lossless output with transparency support, tuned with `pngCompressionLevel`. - **`.webp`** — Efficient compression with both lossless and lossy modes, tuned with `webpQuality`. ### JPEG Thumbnails JPEG works well for most gallery and list previews. Values around `0.8` usually balance image quality and file size for thumbnails. ```swift highlight-createThumbnail-jpeg let jpegThumbnail = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.8, targetWidth: 400, targetHeight: 300), ) print("JPEG thumbnail: \(jpegThumbnail.count) bytes") ``` ### PNG Thumbnails PNG preserves transparency and lossless quality. Increase `pngCompressionLevel` when smaller files matter more than encoding speed — compression affects speed, not quality. ```swift highlight-createThumbnail-png let pngThumbnail = try await engine.block.export( page, mimeType: .png, options: ExportOptions(pngCompressionLevel: 6, targetWidth: 400, targetHeight: 300), ) print("PNG thumbnail: \(pngThumbnail.count) bytes") ``` ### WebP Thumbnails WebP offers efficient compression. A `webpQuality` of `1.0` produces lossless output; lower values enable lossy compression for smaller files. ```swift highlight-createThumbnail-webp let webpThumbnail = try await engine.block.export( page, mimeType: .webp, options: ExportOptions(webpQuality: 0.85, targetWidth: 400, targetHeight: 300), ) print("WebP thumbnail: \(webpThumbnail.count) bytes") ``` ## Show a Preview Without Counting an Export Every export counts toward your export usage. To show a design in your app, for example in a gallery, use `engine.block.exportPreview(_:)` instead. A preview does not count as an export. ```swift highlight-createThumbnail-preview let preview = try await engine.block.exportPreview(page) print("Preview: \(preview.count) bytes") ``` A preview has no options. It is always WebP at quality 0.8 and keeps the block's aspect ratio. A block larger than 262,144 pixels (512 × 512) is scaled down to fit. Use `engine.block.export(_:mimeType:options:)` for a file that you deliver. A preview is always a still image. For a video, it shows one frame. To show frames across a video, see [Thumbnail Previews](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/thumbnail-previews-dcf031/), which doesn't count as an export either. A preview can fail while a video export runs, because the engine renders one export at a time. Wait until the video export finishes, then request the preview again. ## Common Thumbnail Sizes Use target boxes that match the destination UI instead of exporting full-resolution artwork and scaling it afterward. `targetWidth` and `targetHeight` define the box CE.SDK fills, not the guaranteed exact output dimensions — when the source aspect ratio differs, the thumbnail may exceed one axis while preserving that ratio. | Size | Target box | Use case | | ---- | ---------- | -------- | | Small | 150 x 150 | Dense grids and file browsers | | Medium | 400 x 300 | Preview cards and detail lists | | Large | 800 x 600 | Larger preview panels | ## Generate Multiple Sizes Create a responsive thumbnail set by exporting the same page with different dimensions. This keeps each asset close to the size your UI needs. ```swift highlight-createThumbnail-multiple let targetSizes: [(width: Float, height: Float)] = [(150, 150), (400, 300), (800, 600)] var responsiveSet: [Data] = [] for size in targetSizes { let sized = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.8, targetWidth: size.width, targetHeight: size.height), ) responsiveSet.append(sized) print("\(Int(size.width))x\(Int(size.height)): \(sized.count) bytes") } ``` Batching several sizes together also makes it easier to cache or upload them as a group in your app code. ## Save a Thumbnail Exporting produces in-memory image `Data`. To persist a thumbnail, write it to a file in your app's cache or documents directory with `Data`'s `write(to:)`. ```swift highlight-createThumbnail-save let savedThumbnail = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.8, targetWidth: 400, targetHeight: 300), ) let fileURL = outputDirectory.appendingPathComponent("thumbnail.jpg") try savedThumbnail.write(to: fileURL) ``` Writing to a caller-provided file keeps your app in control of storage lifetime, cleanup, and sharing. ## Optimize Thumbnail Quality Tune the format-specific `ExportOptions` fields to balance file size, quality, and export time. | Format | Option | Range | Default | Notes | | ------ | ------ | ----- | ------- | ----- | | JPEG | `jpegQuality` | `(0, 1]` | `0.9` | Lower values reduce file size and may add visible artifacts. | | PNG | `pngCompressionLevel` | `0`–`9` | `5` | Higher values reduce file size but encode more slowly. | | WebP | `webpQuality` | `(0, 1]` | `1.0` | `1.0` is lossless; lower values enable lossy compression. | For thumbnails, start with JPEG quality around `0.8` or WebP quality around `0.75`–`0.85`, then adjust based on your UI and storage constraints. ## Thumbnails from Video Blocks Exporting a page that contains a video fill captures the page's current frame as a single still image. Seek the timeline with `setPlaybackTime(_:time:)` before exporting to choose which frame becomes the thumbnail. Load the video first with `forceLoadAVResource(_:)` so the frame is available. ```swift highlight-createThumbnail-video let videoScene = try engine.scene.createVideo() let videoPage = try engine.block.create(.page) try engine.block.appendChild(to: videoScene, child: videoPage) try engine.block.setWidth(videoPage, value: 1280) try engine.block.setHeight(videoPage, value: 720) try engine.block.setDuration(videoPage, duration: 10) let clip = try engine.block.create(.graphic) try engine.block.setShape(clip, shape: engine.block.createShape(.rect)) let clipFill = try engine.block.createFill(.video) try engine.block.setURL( clipFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) try engine.block.setFill(clip, fill: clipFill) try engine.block.setWidth(clip, value: 1280) try engine.block.setHeight(clip, value: 720) try engine.block.appendChild(to: videoPage, child: clip) try await engine.block.forceLoadAVResource(clipFill) if try engine.block.supportsPlaybackTime(videoPage) { try engine.block.setPlaybackTime(videoPage, time: 2.0) } let videoThumbnail = try await engine.block.export( videoPage, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.8, targetWidth: 400, targetHeight: 225), ) try videoThumbnail.write(to: outputDirectory.appendingPathComponent("video-thumbnail.jpg")) ``` This produces one static image, not a sequence of scrubber frames. ## API Reference ### Methods | Method | Description | | ------ | ----------- | | `engine.scene.getCurrentPage()` | Get the current page block, or `nil` when none is selected. | | `engine.scene.getPages()` | Get all page blocks in the current scene. | | `engine.block.export(_:mimeType:options:)` | Export a page or block to image `Data` with format and dimension options. | | `engine.block.exportPreview(_:)` | Export a small WebP preview as `Data` that does not count as an export. | | `engine.block.forceLoadAVResource(_:)` | Load a video fill's resource before seeking or exporting a frame. | | `engine.block.supportsPlaybackTime(_:)` | Check whether a block exposes a playback timeline. | | `engine.block.setPlaybackTime(_:time:)` | Seek the timeline to a time in seconds before exporting a video frame. | ### Properties | Property | Type | Description | | -------- | ---- | ----------- | | `targetWidth` / `targetHeight` | Float | Scale the export to fill this box while preserving aspect ratio. Set both together. | | `jpegQuality` | Float | JPEG quality in `(0, 1]`. Default `0.9`. | | `pngCompressionLevel` | Int | PNG compression from `0` to `9`. Default `5`. | | `webpQuality` | Float | WebP quality in `(0, 1]`. Default `1.0`. | ## Next Steps - [Export designs to image formats](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Export designs to image, PDF, and video formats. - [Compress exported images](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/compress-29105e/) — Reduce file size and tune quality for thumbnails and previews. - [Batch processing designs](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/) — Generate thumbnails at scale as part of automated workflows. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Export" description: "Explore export options, supported formats, and configuration features for sharing or rendering output." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) --- --- ## Related Pages - [Options](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) - Explore export options, supported formats, and configuration features for sharing or rendering output. - [Export for Social Media](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-social-media-0e8a92/) - Export vertical videos with the correct dimensions, formats, and quality settings for Instagram Reels, TikTok, and YouTube Shorts. - [To MP4](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-mp4-c998a8/) - Export video compositions as MP4 files with H.264 encoding, progress events, and configurable quality and resolution. - [To WebM](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-webm-764271/) - Export video compositions as WebM files with the royalty-free VP8, VP9 or AV1 video codecs and Opus or Vorbis audio, with optional transparency. - [For Audio Processing](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/audio-68de25/) - Learn how to export audio in WAV or MP4 format from any block type in CE.SDK for iOS and macOS. - [To PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) - Export designs as PDF documents with high compatibility mode and underlayer support for special media printing. - [To JPEG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-jpeg-6f88e9/) - Export CE.SDK designs to JPEG format with configurable quality settings for photographs, web images, and social media content. - [To PNG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-png-f87eaf/) - Export CE.SDK designs to PNG format with lossless compression and full alpha support for graphics, UI elements, and content with transparency. - [To WebP](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-webp-aef6f4/) - Export CE.SDK designs to WebP format with lossy and lossless compression for smaller files than PNG or JPEG at comparable quality. - [To Raw Data](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-raw-data-abd7da/) - Export CE.SDK designs to uncompressed RGBA pixel data for custom image processing, Core Graphics rendering, and integration with advanced imaging pipelines. - [Compress Exports for Smaller Files](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/compress-29105e/) - Learn how to reduce file sizes during export from CE.SDK for iOS, macOS, and Catalyst by tuning format-specific compression settings. - [Export with a Color Mask](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/with-color-mask-4f868f/) - Export design blocks with color masking in CE.SDK to remove specific colors and generate alpha masks for print workflows and compositing. - [Pre-Export Validation](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/pre-export-validation-3a2cba/) - Documentation for Pre-Export Validation - [Partial Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/) - Export individual blocks, grouped elements, or specific pages from a CE.SDK scene in Swift instead of exporting the whole scene. - [Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/) - Configure and understand CE.SDK's image and video size limits in Swift to balance quality and performance across devices. - [Create Thumbnail](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/create-thumbnail-749be1/) - Generate thumbnail preview images from CE.SDK scenes by exporting with target dimensions for galleries and design management. - [Thumbnail Previews](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/thumbnail-previews-dcf031/) - Stream video filmstrips, page storyboards, and audio waveforms in Swift with the CE.SDK thumbnail sequence APIs. - [Export for Printing](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-printing-bca896/) - Export designs from CE.SDK as print-ready PDFs with professional output options including high compatibility mode, underlayers for special media, and scene DPI configuration. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "For Audio Processing" description: "Learn how to export audio in WAV or MP4 format from any block type in CE.SDK for iOS and macOS." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/audio-68de25/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [For Audio Processing](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/audio-68de25/) --- Export audio from pages, video blocks, audio blocks, and tracks to WAV or MP4 format for external processing, transcription, or analysis. The `exportAudio` API allows you to extract audio from any block that contains audio content. This is particularly useful when integrating with external audio processing services like speech-to-text transcription, audio enhancement, or music analysis platforms. Audio can be exported from multiple block types: - **Page blocks** - Export the complete mixed audio timeline - **Video blocks** - Extract audio tracks from videos - **Audio blocks** - Export standalone audio content - **Track blocks** - Export audio from specific timeline tracks ## Export Audio Export audio from any block using the `exportAudio` API: ```swift let page = try engine.scene.getCurrentPage() let audioData = try await engine.block.exportAudio( page, mimeType: .wav, sampleRate: 48000, numberOfChannels: 2 ) print("Exported \(audioData.count) bytes") ``` ### Export Options Configure your audio export with these parameters: - **`mimeType`** - `.wav` (uncompressed) or `.mp4` (compressed AAC) - **`sampleRate`** - Audio quality in Hz (default: 48000) - **`numberOfChannels`** - 1 for mono or 2 for stereo - **`timeOffset`** - Start time in seconds (default: 0.0) - **`duration`** - Length to export in seconds (0.0 = entire duration) - **`onProgress`** - Callback receiving `(rendered, encoded, total)` for progress tracking ## Find Audio Sources To find blocks with audio in your scene: ```swift // Find audio blocks let audioBlocks = try engine.block.findByType(.audio) // Find video fills with audio let videoFills = try engine.block.findByType(.videoFill) let videosWithAudio = videoFills.filter { block in do { return try !engine.block.getAudioInfoFromVideo(block).isEmpty } catch { return false } } ``` ## Working with Multi-Track Video Audio Videos can contain multiple audio tracks (e.g., different languages). CE.SDK provides APIs to inspect and extract specific tracks. ### Check audio track count ```swift guard let videoFillId = try engine.block.findByType(.videoFill).first else { throw AudioExportError.noVideoFound } let trackCount = try engine.block.getAudioTrackCountFromVideo(videoFillId) print("Video has \(trackCount) audio track(s)") ``` ### Get track information ```swift let audioTracks = try engine.block.getAudioInfoFromVideo(videoFillId) for (index, track) in audioTracks.enumerated() { print(""" Track \(index): - Channels: \(track.channels) // 1=mono, 2=stereo - Sample Rate: \(track.sampleRate) Hz - Language: \(track.language ?? "unknown") - Label: \(track.label ?? "Track \(index)") """) } ``` ### Extract a specific track ```swift // Create audio block from track 0 (first track) let audioBlockId = try engine.block.createAudioFromVideo(videoFillId, trackIndex: 0) // Export just this track's audio let trackAudioData = try await engine.block.exportAudio( audioBlockId, mimeType: .wav, sampleRate: 48000, numberOfChannels: 2 ) ``` ### Extract all tracks ```swift // Create audio blocks for all tracks let audioBlockIds = try engine.block.createAudiosFromVideo(videoFillId) // Export each track for (i, audioBlockId) in audioBlockIds.enumerated() { let trackData = try await engine.block.exportAudio(audioBlockId, mimeType: .wav) print("Track \(i): \(trackData.count) bytes") } ``` ## Complete Workflow: Audio to Captions A common workflow is to export audio, send it to a transcription service, and use the returned captions in your scene. ### Step 1: Export Audio ```swift let page = try engine.scene.getCurrentPage() let audioData = try await engine.block.exportAudio( page, mimeType: .wav, sampleRate: 48000, numberOfChannels: 2 ) ``` ### Step 2: Send to Transcription Service Send the audio to a service that returns SubRip (SRT) format captions: Provide the endpoint and authorization header through your app's configuration. Point the endpoint at an app-owned backend or proxy, and keep third-party service credentials out of the client. ```swift func transcribeAudio( _ audioData: Data, endpoint: URL, authorizationHeader: String ) async throws -> String { let boundary = UUID().uuidString var body = Data() // Add audio file body.append("--\(boundary)\r\n") body.append("Content-Disposition: form-data; name=\"audio\"; filename=\"audio.wav\"\r\n") body.append("Content-Type: audio/wav\r\n\r\n") body.append(audioData) body.append("\r\n") // Add format parameter body.append("--\(boundary)\r\n") body.append("Content-Disposition: form-data; name=\"format\"\r\n\r\n") body.append("srt") body.append("\r\n--\(boundary)--\r\n") var request = URLRequest(url: endpoint) request.httpMethod = "POST" request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") request.setValue(authorizationHeader, forHTTPHeaderField: "Authorization") request.httpBody = body let (data, _) = try await URLSession.shared.data(for: request) return String(data: data, encoding: .utf8) ?? "" } extension Data { mutating func append(_ string: String) { if let data = string.data(using: .utf8) { append(data) } } } let srtContent = try await transcribeAudio( audioData, endpoint: transcriptionEndpoint, authorizationHeader: transcriptionAuthorizationHeader ) ``` ### Step 3: Import Captions from SRT Use the built-in API to create caption blocks from the SRT response: ```swift import Foundation // Save SRT to temporary file let tempDir = FileManager.default.temporaryDirectory let tempFile = tempDir.appendingPathComponent("captions.srt") try srtContent.write(to: tempFile, atomically: true, encoding: .utf8) // Import captions from file URL let captions = try await engine.block.createCaptionsFromURI(tempFile.absoluteString) // Clean up temporary file try FileManager.default.removeItem(at: tempFile) // Add captions to page let page = try engine.scene.getCurrentPage() let captionTrack = try engine.block.create(.captionTrack) for caption in captions { try engine.block.appendChild(to: captionTrack, child: caption) } try engine.block.appendChild(to: page, child: captionTrack) // Center the first caption as a reference point try engine.block.alignHorizontally([captions[0]], alignment: .center) try engine.block.alignVertically([captions[0]], alignment: .center) ``` ### Other Processing Services Audio export also supports these workflows: - **Audio enhancement** - Noise removal, normalization - **Music analysis** - Tempo, key, beat detection - **Language detection** - Identify spoken language - **Speaker diarization** - Identify who spoke when ## Next Steps Now that you understand audio export, explore related audio and video features in the [Create Video guides](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/). --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Compress Exports for Smaller Files" description: "Learn how to reduce file sizes during export from CE.SDK for iOS, macOS, and Catalyst by tuning format-specific compression settings." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/compress-29105e/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [Compress](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/compress-29105e/) --- Compressions goal is to reduce file sizes during export while maintaining as much visual quality as possible. With the CreativeEditor SDK (CE.SDK) for Swift, you can fine-tune compression settings for both images and videos. This allows your app to balance performance, quality, and storage efficiency across iOS, macOS, and Catalyst. ## What You’ll Learn - How to configure compression for PNG, JPEG, and WebP exports. - How to control video file size using bitrate and resolution scaling. - How to balance file size, quality, and export performance for different use cases. - How to configure compression programmatically during automation or batch operations. ## When to Use It Compression tuning is useful whenever: - Exported media is too large for upload limits - You need to optimize storage quotas - You have constrained network bandwidth Use it when preparing images or videos for any workflow that benefits from: - Faster load times and smaller files, like: - Social media - Web delivery - Consistent file size and predictable performance, like: - Batch export - Automation scenarios ## Understanding Compression Options by Format Each format supports its own parameters for balancing: - Speed - File size - Quality You pass these through the `ExportOptions` or `VideoExportOptions` structure when calling the export functions. | Format | Parameter | Type | Effect | Default | | ------- | ---------- | ---- | ------- | -------- | | PNG | `pngCompressionLevel` | 0–9 | Higher = smaller, slower (lossless) | 5 | | JPEG | `jpegQuality` | 0.0–1.0 | Lower = smaller, lower quality | 0.9 | | WebP | `webpQuality` | 0.0–1.0 | 1.0 = lossless, \<1.0 = lossy | 1.0 | | PDF | `pdfImageQuality` | 0.0–1.0 | 1.0 = lossless, \<1.0 = lossy JPEG for rasterized images | 1.0 | | MP4 | `videoBitrate`, `audioBitrate` | bits/sec or `VideoBitrate.system` / `VideoBitrate.auto` | Higher = larger, higher quality | `VideoBitrate.system` | ## Export Images with Compression Below is an example that exports a design block as PNG and JPEG while tuning compression options. ```swift import Foundation import IMGLYEngine #if canImport(UIKit) import UIKit #endif @MainActor func exportCompressedImages(engine: Engine) async throws { // Load a demo scene let sceneURL = URL(string: "https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene")! let scene = try await engine.scene.load(from: sceneURL) // Select the first graphic block to export let block = try engine.block.find(byType: .graphic).first! // Export PNG with maximum compression (lossless) let pngOptions = ExportOptions(pngCompressionLevel: 9) let pngData = try await engine.block.export(block, mimeType: .png, options: pngOptions) // Export JPEG with balanced quality (lossy) let jpegOptions = ExportOptions(jpegQuality: 0.7) let jpegData = try await engine.block.export(block, mimeType: .jpeg, options: jpegOptions) // Convert to UIImage for preview (iOS) // pass these to another part of the app for preview let pngImage = UIImage(data: pngData) let jpegImage = UIImage(data: jpegData) } ``` Choose a format depending on what matters the most for your output: - **PNG** is ideal for flat graphics or assets that require **transparency**. - **JPEG** is best for photographs where slight **compression** artifacts are acceptable. - **WebP** can serve **both** roles: it supports transparency like PNG and delivers smaller files like JPEG. ## Combine Compression with Resolution Scaling You can further reduce file size by downscaling exports: ```swift let scaledOptions = ExportOptions( pngCompressionLevel: 7, targetWidth: 1080, targetHeight: 1080 ) let scaledBlob = try await engine.block.export(block, mimeType: .png, options: scaledOptions) ``` When you specify only one dimension, CE.SDK automatically preserves aspect ratio for consistent results. ## Compress Video Exports The `VideoExportOptions` structure handles configuration for video compression. You can specify: - Bitrate - Framerate - H.264 profile - Target resolution ```swift let videoOptions = VideoExportOptions( h264Profile: .main, h264Level: 52, videoBitrate: 2_000_000, // 2 Mbps = moderate compression audioBitrate: 128_000, // 128 kbps AAC framerate: 30.0, targetWidth: 1280, targetHeight: 720 ) // Export a page as compressed MP4 if let page = try engine.scene.getCurrentPage() { for try await export in try await engine.block.exportVideo(page, mimeType: .mp4, options: videoOptions) { switch export { case let .progress(_, encodedFrames, totalFrames): print("Progress: \(encodedFrames)/\(totalFrames)") case let .finished(video: videoData): print("Export complete: \(videoData.count) bytes") } } } ``` About the bitrate’s values: - **1–2 Mbps** produces high quality results for **web** and social media clips. - **8–12 Mbps** is more appropriate for **downloadable HD video**. Setting `videoBitrate` to `VideoBitrate.auto` (`-1`) lets CE.SDK choose a bounded, optimized bitrate based on resolution and frame rate. The default `VideoBitrate.system` (`0`) instead lets the platform encoder (VideoToolbox) decide. The H.264 `profile` and `level` determine compatibility and encoder features.\ Use `.baseline` for mobile-friendly playback, `.main` for standard HD, and `.high` for the highest quality exports targeting desktop or professional workflows. ## Performance and Trade-Offs Higher compression results in smaller files but slower export speeds. For example: - PNG Level 9 may take twice as long to encode as Level 3–5, though it produces smaller files. - JPEG and WebP are faster but can introduce visible compression artifacts. Video exports are more demanding and depend heavily on device CPU and GPU performance. You can check available export limits before encoding: ```swift let maxSize = try engine.editor.getMaxExportSize() let availableMemory = try engine.editor.getAvailableMemory() print("Max export size: \(maxSize), Memory: \(availableMemory)") ``` ## Real-World Compression Comparison (1080 × 1080) The following table compares average results across different compression settings for photo-like and graphic-like images. | Format | Setting | Avg. File Size (KB) | Encode Time (ms) | PSNR (dB)\* | Notes | | ------- | -------- | ------------------- | ---------------- | ------------ | ------ | | **PNG** | Level 0 | ~1 450 | ~44 | ∞ (lossless) | Fastest, largest | | | Level 5 | ~1 260 | ~61 | ∞ | Balanced speed and size | | | Level 9 | ~1 080 | ~88 | ∞ | Smallest, slowest | | **JPEG** | Quality 95 | ~640 | ~24 | 43 | Near-lossless appearance | | | Quality 80 | ~420 | ~20 | 39 | Good default for photos | | | Quality 60 | ~290 | ~17 | 35 | Some artifacts visible | | | Quality 40 | ~190 | ~15 | 31 | Heavy compression | | **WebP** | Quality 95 | ~510 | ~27 | 44 | Smaller than JPEG | | | Quality 80 | ~350 | ~23 | 39 | Excellent web balance | | | Quality 60 | ~240 | ~20 | 35 | Mild artifacts | | | Quality 40 | ~160 | ~18 | 31 | Compact, noticeable loss | | | Lossless | ~830 | ~33 | ∞ | Smaller than PNG, keeps alpha | \*PSNR > 40 dB ≈ visually lossless; 30–35 dB shows mild artifacts. **Key Takeaways**: - **WebP** achieves 70–85 % smaller files than uncompressed PNG with high quality around `webpQuality = 0.8`. - **JPEG** performs well for photographs; use `jpegQuality = 0.8–0.9` for web or print, `0.6` for compact exports. - **PNG** is essential for transparency and vector-like shapes; higher levels reduce size modestly at the cost of speed. - Test on realistic assets: complex photos and flat graphics compress differently. ## Practical Presets These presets provide starting points for common export scenarios. | Use Case | Format | Typical Settings | Result | Notes | |-----------|---------|------------------|---------|-------| | **Web or Social Sharing** | JPEG / WebP | `jpegQuality: 0.8` or `webpQuality: 0.8` | ~60–70 % smaller than PNG | Balanced quality and size | | **UI Graphics / Transparent Assets** | PNG / WebP | `pngCompressionLevel: 6–8` or `webpQuality: 1.0 (lossless)` | ~25 % smaller than default PNG | Maintains transparency | | **High-Quality Print or Archival** | PNG / WebP Lossless | `pngCompressionLevel: 9` or `webpQuality: 1.0` | Maximum fidelity | Slower export, large files | | **Video for Web / Social** | MP4 | `videoBitrate: 2_000_000`, `audioBitrate: 128_000`, `targetWidth: 1280` | Smooth playback, small file | Adjust for platform | | **Video for Download / HD** | MP4 | `videoBitrate: 8_000_000`, `targetWidth: 1920`, `framerate: 30` | Full HD quality | Larger file, slower encode | **PDF and Print**: PDF exports are not compressed by default. Use `exportPdfWithHighCompatibility` when you need broad software support in print workflows. With that flag disabled, CE.SDK embeds unmodified JPEG images with their original data instead of rasterizing them. Set `pdfImageQuality` below `1.0` to encode the images that CE.SDK still has to rasterize as lossy JPEG. Only rasterized content that is fully opaque can be re-encoded, because JPEG has no alpha channel. Content that keeps transparency, for example when you export a block or a page without a background fill, stays lossless whatever you set this option to. > **Note:** Consider showing users an **estimated file size** before export. It helps them make informed choices about quality vs. performance. ## Automating Compression in Batch Exports When exporting multiple elements, apply the same compression settings programmatically: ```swift for block in try engine.block.find(byType: .graphic) { let options = ExportOptions(jpegQuality: 0.8) _ = try await engine.block.export(block, mimeType: .jpeg, options: options) } ``` This ensures consistent quality and file size across all exported assets. ## Troubleshooting **❌ File size not reduced**: - Ensure correct property name such as`jpegQuality`, `webpQuality`. **❌ JPEG Quality too low**: - Increase quality to 0.9 or use PNG/WebP lossless. **❌ Export slow**: - Check for excessive compression level. - Lower PNG level to 5–6. **❌ Video not compressing**: - Set `videoBitrate` to a non-zero reasonable value. ## Next Steps Compression is one of the most practical tools for optimizing export workflows.\ By adjusting the `ExportOptions` and `VideoExportOptions` structures in Swift, you can deliver high-quality results efficiently—whether your users are exporting social media posts, UI assets, or professional-grade print layouts. - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) to learn about all available export formats. - Apply compression consistently in automated exports using [batch processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/). - Combine scaling and compression for [thumbnails](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/create-thumbnail-749be1/). --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Options" description: "Explore export options, supported formats, and configuration features for sharing or rendering output." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) --- Export your designs to multiple formats including PNG, JPEG, WebP, SVG, PDF, MP4, and WAV. CE.SDK handles all export processing on-device, giving you fine-grained control over format-specific options like compression, quality, and target dimensions. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-export-overview) Whether you're building a design tool, photo editor, or batch rendering pipeline, understanding export options helps you deliver the right output for each use case. This guide covers the supported formats, their options, and how to export programmatically. ```swift file=@cesdk_swift_examples/engine-guides-export-overview/ExportOverview.swift reference-only import Foundation import IMGLYEngine @MainActor func exportOverview(engine: Engine) async throws { // Demo scaffolding: build a two-page scene with renderable content so every // highlighted snippet has something to export. In your app you would start // from a scene already loaded into the editor instead. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.setDuration(page, duration: 1.0) try engine.block.appendChild(to: scene, child: page) let rectangle = try engine.block.create(.graphic) try engine.block.setShape(rectangle, shape: engine.block.createShape(.rect)) let rectangleFill = try engine.block.createFill(.color) try engine.block.setColor( rectangleFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0), ) try engine.block.setFill(rectangle, fill: rectangleFill) try engine.block.setPositionX(rectangle, value: 100) try engine.block.setPositionY(rectangle, value: 100) try engine.block.setWidth(rectangle, value: 600) try engine.block.setHeight(rectangle, value: 400) try engine.block.appendChild(to: page, child: rectangle) let secondPage = try engine.block.create(.page) try engine.block.setWidth(secondPage, value: 800) try engine.block.setHeight(secondPage, value: 600) try engine.block.setDuration(secondPage, duration: 1.0) try engine.block.appendChild(to: scene, child: secondPage) let ellipse = try engine.block.create(.graphic) try engine.block.setShape(ellipse, shape: engine.block.createShape(.ellipse)) let ellipseFill = try engine.block.createFill(.color) try engine.block.setColor( ellipseFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.2, b: 0.2, a: 1.0), ) try engine.block.setFill(ellipse, fill: ellipseFill) try engine.block.setPositionX(ellipse, value: 100) try engine.block.setPositionY(ellipse, value: 100) try engine.block.setWidth(ellipse, value: 600) try engine.block.setHeight(ellipse, value: 400) try engine.block.appendChild(to: secondPage, child: ellipse) // Audio block backed by an in-memory buffer so the audio export below has // something to read without making a network request. let audioBlock = try engine.block.create(.audio) try engine.block.appendChild(to: page, child: audioBlock) // A new audio block has no end, so give it a length before exporting it. try engine.block.setDuration(audioBlock, duration: 1.0) let audioBuffer = engine.editor.createBuffer() try engine.editor.setBufferLength(url: audioBuffer, length: 96000) try engine.block.setURL(audioBlock, property: "audio/fileURI", value: audioBuffer) let exportsDirectory = FileManager.default.temporaryDirectory let pngOptions = ExportOptions(pngCompressionLevel: 9) let pngBlob = try await engine.block.export(page, mimeType: .png, options: pngOptions) try pngBlob.write(to: exportsDirectory.appendingPathComponent("design.png")) let jpegOptions = ExportOptions(jpegQuality: 0.9) let jpegBlob = try await engine.block.export(page, mimeType: .jpeg, options: jpegOptions) try jpegBlob.write(to: exportsDirectory.appendingPathComponent("design.jpg")) let webpOptions = ExportOptions(webpQuality: 1.0) let webpBlob = try await engine.block.export(page, mimeType: .webp, options: webpOptions) try webpBlob.write(to: exportsDirectory.appendingPathComponent("design.webp")) let svgBlob = try await engine.block.export(page, mimeType: .svg) try svgBlob.write(to: exportsDirectory.appendingPathComponent("design.svg")) let pdfOptions = ExportOptions(exportPdfWithHighCompatibility: true) let pdfBlob = try await engine.block.export(page, mimeType: .pdf, options: pdfOptions) try pdfBlob.write(to: exportsDirectory.appendingPathComponent("design.pdf")) let maskedBlobs = try await engine.block.exportWithColorMask( page, mimeType: .png, maskColorR: 1.0, maskColorG: 0.0, maskColorB: 0.0, ) try maskedBlobs[0].write(to: exportsDirectory.appendingPathComponent("design.masked.png")) try maskedBlobs[1].write(to: exportsDirectory.appendingPathComponent("design.alpha.png")) let videoOptions = VideoExportOptions( h264Profile: .main, framerate: 30, targetWidth: 1280, targetHeight: 720, ) let videoStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: videoOptions) for try await event in videoStream { switch event { case let .progress(rendered, encoded, total): print("Video export: \(encoded)/\(total) frames encoded (\(rendered) rendered)") case let .finished(video): try video.write(to: exportsDirectory.appendingPathComponent("design.mp4")) } } let audioStream = try await engine.block.exportAudio(audioBlock, mimeType: .wav) for try await event in audioStream { if case let .finished(audio) = event { try audio.write(to: exportsDirectory.appendingPathComponent("design.wav")) } } let resizedOptions = ExportOptions(targetWidth: 1080, targetHeight: 1080) let resizedBlob = try await engine.block.export(page, mimeType: .png, options: resizedOptions) try resizedBlob.write(to: exportsDirectory.appendingPathComponent("design.1080.png")) let maxExportSize = try engine.editor.getMaxExportSize() let availableMemory = try? engine.editor.getAvailableMemory() print("Max export size: \(maxExportSize)px") if let availableMemory { print("Available memory: \(availableMemory) bytes") } } ``` ## Supported Export Formats CE.SDK supports exporting scenes, pages, groups, or individual blocks in these formats: | Format | MIME Type | Transparency | Best For | | ------ | -------------------------- | -------------- | ------------------------------------------------- | | PNG | `image/png` | Yes | Web graphics, UI elements, logos | | JPEG | `image/jpeg` | No | Photographs, web images | | WebP | `image/webp` | Yes (lossless) | Smaller files than PNG with comparable fidelity | | SVG | `image/svg+xml` | Yes | Scalable graphics, post-processing | | TGA | `image/x-tga` | Yes | Image pipelines that require TGA files | | PDF | `application/pdf` | Partial | Print, documents | | MP4 | `video/mp4` | No | Animated content | | WebM | `video/webm` | Yes | Animated content with royalty-free codecs | | WAV | `audio/wav` | — | Lossless audio | | Binary | `application/octet-stream` | Yes | Raw RGBA8888 data for further processing | Each format serves different purposes. PNG preserves transparency and works well for graphics with sharp edges or text. JPEG compresses photographs efficiently but drops transparency. WebP provides excellent compression with optional lossless mode. SVG produces scalable vector output ideal for post-processing with standard SVG tooling. PDF preserves vector information for print workflows. MP4 exports animated content as H.264 video, and WAV exports lossless audio tracks. For the complete format compatibility matrix, including codecs and size limits, see [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/file-format-support-3c4b2a/). ## Export Images ### Export to PNG PNG export uses lossless compression with a configurable compression level. Higher compression produces smaller files but takes longer to encode. Quality is not affected. ```swift highlight-exportOverview-png let pngOptions = ExportOptions(pngCompressionLevel: 9) let pngBlob = try await engine.block.export(page, mimeType: .png, options: pngOptions) try pngBlob.write(to: exportsDirectory.appendingPathComponent("design.png")) ``` The `pngCompressionLevel` ranges from 0 (no compression, fastest) to 9 (maximum compression, slowest). The default is 5, which balances file size and encoding speed. ### Export to JPEG JPEG export uses lossy compression controlled by the quality setting. Lower quality produces smaller files but introduces visible artifacts. ```swift highlight-exportOverview-jpeg let jpegOptions = ExportOptions(jpegQuality: 0.9) let jpegBlob = try await engine.block.export(page, mimeType: .jpeg, options: jpegOptions) try jpegBlob.write(to: exportsDirectory.appendingPathComponent("design.jpg")) ``` The `jpegQuality` ranges from 0 to 1. Values above 0.9 provide excellent quality for most use cases. The default is 0.9. > **Caution:** JPEG drops transparency from exports. Transparent areas render with a solid background, which may produce unexpected results for designs that rely on alpha channels. ### Export to WebP WebP provides better compression than PNG or JPEG. A quality of 1.0 enables lossless mode. ```swift highlight-exportOverview-webp let webpOptions = ExportOptions(webpQuality: 1.0) let webpBlob = try await engine.block.export(page, mimeType: .webp, options: webpOptions) try webpBlob.write(to: exportsDirectory.appendingPathComponent("design.webp")) ``` The `webpQuality` ranges from 0 to 1. At 1.0, WebP uses lossless compression that typically produces smaller files than equivalent PNG exports. ### Export to SVG SVG export produces scalable vector graphics that integrate with standard SVG tooling and scale to any resolution without quality loss. ```swift highlight-exportOverview-svg let svgBlob = try await engine.block.export(page, mimeType: .svg) try svgBlob.write(to: exportsDirectory.appendingPathComponent("design.svg")) ``` Text is exported as vector paths to ensure consistent rendering without requiring the original fonts. Shapes, strokes, and gradients are exported as native SVG elements. > **Note:** Drop shadows, blur, effects (filters, adjustments), and raster images cannot be represented as native SVG vector elements. These features are rasterized and embedded as PNG images within the SVG. This preserves visual fidelity but increases file size and means those parts of the output are not scalable. > **Note:** SVG export does not apply track mattes or a blur in `Block` mode. A block with a matte exports whole, and a block with a `Block` blur exports unblurred. > **Note:** SVG export renders a single page. To export a multi-page scene, export each page individually. ### Image Export Options | Option | Type | Default | Description | | --------------------- | ------- | ------- | -------------------------------------------------------------------- | | `pngCompressionLevel` | `Int` | `5` | PNG compression level (0–9). Higher = smaller file, slower encoding. | | `jpegQuality` | `Float` | `0.9` | JPEG quality (0–1). Higher = better quality, larger file. | | `webpQuality` | `Float` | `1.0` | WebP quality (0–1). Set to `1.0` for lossless compression. | | `targetWidth` | `Float` | `0` | Target output width in pixels. `0` keeps the block's natural width. | | `targetHeight` | `Float` | `0` | Target output height in pixels. `0` keeps the block's natural height.| | `allowTextOverhang` | `Bool` | `false` | Include text bounding boxes that account for glyph overhangs. | ## Export PDF PDF export preserves vector information and supports print workflows. The high compatibility option rasterizes images and effects for broader viewer support. ```swift highlight-exportOverview-pdf let pdfOptions = ExportOptions(exportPdfWithHighCompatibility: true) let pdfBlob = try await engine.block.export(page, mimeType: .pdf, options: pdfOptions) try pdfBlob.write(to: exportsDirectory.appendingPathComponent("design.pdf")) ``` When `exportPdfWithHighCompatibility` is `true` (the default), images and effects are rasterized according to the scene's DPI setting. Set it to `false` for faster exports, though gradients with transparency may not render correctly in some PDF viewers. With high compatibility disabled, CE.SDK embeds unmodified JPEG images with their original data, which keeps photo-heavy exports small. Use `pdfImageQuality` to encode the images that CE.SDK still has to rasterize as lossy JPEG. The underlayer options are useful for print workflows where you need a solid base layer (often white ink) beneath the design. The `underlayerSpotColorName` should match a spot color defined in your print workflow. ### PDF Export Options | Option | Type | Default | Description | | -------------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------- | | `exportPdfWithHighCompatibility` | `Bool` | `true` | Rasterize images, effects and the gradients that some viewers draw wrong according to the scene's DPI setting for broader viewer support. | | `pdfImageQuality` | `Float` | `1.0` | Encoding quality for images that have to be rasterized. Values below `1.0` encode them as lossy JPEG. | | `exportPdfWithUnderlayer` | `Bool` | `false` | Add an underlayer behind existing elements matching the shape of page content. | | `underlayerSpotColorName` | `String` | `""` | Spot color name for the underlayer fill (used with print workflows). | | `underlayerOffset` | `Float` | `0` | Size adjustment for the underlayer shape in design units. | | `underlayerRenderRatio` | `Float` | `1.0` | Resolution multiplier for the raster pass that extracts the underlayer contour. | | `underlayerMaxError` | `Float` | `2.0` | Maximum acceptable curve-fit error, in pixels, when vectorising the underlayer contour. Smaller = tighter fit. | | `targetWidth` | `Float` | `0` | Target output width in pixels. | | `targetHeight` | `Float` | `0` | Target output height in pixels. | ## Export with Color Mask Color mask export removes pixels matching a specific RGB color and returns two outputs: the masked image with transparency applied, and an alpha mask showing which pixels were removed. ```swift highlight-exportOverview-colorMask let maskedBlobs = try await engine.block.exportWithColorMask( page, mimeType: .png, maskColorR: 1.0, maskColorG: 0.0, maskColorB: 0.0, ) try maskedBlobs[0].write(to: exportsDirectory.appendingPathComponent("design.masked.png")) try maskedBlobs[1].write(to: exportsDirectory.appendingPathComponent("design.alpha.png")) ``` `exportWithColorMask` accepts the block to export, three RGB color components in the 0.0–1.0 range, and optional export options. RGB values use floating-point notation where 1.0 equals 255 in standard color notation. Common mask colors for print workflows: - Pure red: `(1.0, 0.0, 0.0)` — Registration marks - Pure magenta: `(1.0, 0.0, 1.0)` — Distinctive marker color - Pure cyan: `(0.0, 1.0, 1.0)` — Alternative marker color The method returns an array of two `Blob` values: the masked image (with matched pixels made transparent) and the alpha mask (black pixels for removed areas, white for retained areas). > **Note:** Color matching is exact. Only pixels with RGB values precisely matching the specified color are removed. Anti-aliased edges or color variations are not affected. ### Color Mask Export Options `exportWithColorMask` accepts the same options as image export: | Option | Type | Default | Description | | --------------------- | ------- | ------- | -------------------------------------------------------- | | `pngCompressionLevel` | `Int` | `5` | PNG compression level (0–9). | | `jpegQuality` | `Float` | `0.9` | JPEG quality (0–1). | | `webpQuality` | `Float` | `1.0` | WebP quality (0–1). | | `targetWidth` | `Float` | `0` | Target output width in pixels. | | `targetHeight` | `Float` | `0` | Target output height in pixels. | ## Export Video Video export uses the H.264 codec and outputs MP4 files. `exportVideo` returns an `AsyncThrowingStream` that yields `.progress` events while encoding and a final `.finished(video:)` event with the encoded data. On iOS the export is automatically suspended when the app moves to the background and resumed when it returns to the foreground. ```swift highlight-exportOverview-video let videoOptions = VideoExportOptions( h264Profile: .main, framerate: 30, targetWidth: 1280, targetHeight: 720, ) let videoStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: videoOptions) for try await event in videoStream { switch event { case let .progress(rendered, encoded, total): print("Video export: \(encoded)/\(total) frames encoded (\(rendered) rendered)") case let .finished(video): try video.write(to: exportsDirectory.appendingPathComponent("design.mp4")) } } ``` ### Video Export Options | Option | Type | Default | Description | | ------------------- | ------------- | ------------ | --------------------------------------------------------------------------------- | | `h264Profile` | `H264Profile` | `.main` | Encoder profile: `.baseline` (66), `.main` (77), `.extended` (88), `.high` (100). | | `h264Level` | `Int32` | `52` | Encoding level (multiply desired level by 10, e.g., `52` for level 5.2). | | `videoBitrate` | `Int32` | `VideoBitrate.system` | Video bitrate in bits/second, or a named automatic mode. `VideoBitrate.system` (`0`, the default) lets VideoToolbox choose; `VideoBitrate.auto` (`-1`) picks a bounded, resolution-aware bitrate. A positive number sets an explicit bitrate (maximum determined by profile and level). | | `audioBitrate` | `Int32` | `0` (auto) | Audio bitrate in bits/second. `0` defaults to 128 kbps for stereo AAC. | | `framerate` | `Float?` | `nil` | Target framerate in Hz. `nil` uses the scene's `scene/frameRate`. | | `targetWidth` | `Float` | `0` | Output width in pixels. | | `targetHeight` | `Float` | `0` | Output height in pixels. | | `timeOffset` | `Double` | `0` | Start time offset in seconds. | | `duration` | `Double` | block length | Video duration in seconds. `0` defaults to the duration of the exported page. | | `allowTextOverhang` | `Bool` | `false` | Include text bounding boxes that account for glyph overhangs. | The `h264Profile` determines encoder quality and compatibility: - **Baseline (66)**: Broadest device compatibility, lowest quality. - **Main (77)**: Good balance of quality and compatibility (default). - **High (100)**: Best quality, may not play on older devices. The `videoBitrate` option accepts a positive number (explicit bits per second) or one of two named modes. `VideoBitrate.system` (`0`, the default) lets the platform encoder (VideoToolbox) choose. `VideoBitrate.auto` (`-1`) selects a bounded bitrate derived from the output resolution and framerate (for example 5 Mbps at 720p30, 8 Mbps at 1080p30, 12 Mbps at 1080p60, 40 Mbps at 4K30), consistent across platforms. > **Caution:** H.264 does not support transparency. Transparent areas render with a black background. ## Export Audio Export audio tracks from pages or audio blocks. Supported MIME types are `.wav` (uncompressed) and `.mp4` (AAC encoded). `exportAudio` returns an `AsyncThrowingStream` that yields `.progress` events while encoding and a final `.finished(audio:)` event with the encoded data. ```swift highlight-exportOverview-audio let audioStream = try await engine.block.exportAudio(audioBlock, mimeType: .wav) for try await event in audioStream { if case let .finished(audio) = event { try audio.write(to: exportsDirectory.appendingPathComponent("design.wav")) } } ``` ### Audio Export Options | Option | Type | Default | Description | | ------------------ | -------- | ------------ | -------------------------------------------------------------------------------------------- | | `sampleRate` | `Int32` | `48000` | Sample rate in Hz. | | `numberOfChannels` | `Int32` | `2` | Number of audio channels (`1` mono, `2` stereo). | | `timeOffset` | `Double` | `0` | Start time offset in seconds, relative to the block. | | `duration` | `Double` | block length | Audio duration in seconds. `0` defaults to the duration of the exported block. | | `skipEncoding` | `Bool` | `false` | Copy the raw 48 kHz stereo float samples of a buffer into a WAV file without encoding them. Any other export runs as it does without this option. | Use `.wav` for lossless quality when file size is not a concern. Use `.mp4` (AAC) for compressed output. > **Note:** Audio export extracts and processes audio from all audio-capable blocks within the target block, including video fills with audio tracks and standalone audio blocks. ## Target Size Control You can export at specific dimensions regardless of the block's actual size. The `targetWidth` and `targetHeight` options render the block large enough to fill the target size while maintaining aspect ratio. ```swift highlight-exportOverview-targetSize let resizedOptions = ExportOptions(targetWidth: 1080, targetHeight: 1080) let resizedBlob = try await engine.block.export(page, mimeType: .png, options: resizedOptions) try resizedBlob.write(to: exportsDirectory.appendingPathComponent("design.1080.png")) ``` If the target aspect ratio differs from the block's aspect ratio, the output fills the target dimensions completely. The output may extend beyond the target size on one axis to preserve correct proportions. ## Device Export Limits Before exporting large designs, check the device's export capabilities. Memory constraints or GPU limitations may prevent exports that exceed certain dimensions. ```swift highlight-exportOverview-checkLimits let maxExportSize = try engine.editor.getMaxExportSize() let availableMemory = try? engine.editor.getAvailableMemory() print("Max export size: \(maxExportSize)px") if let availableMemory { print("Available memory: \(availableMemory) bytes") } ``` `getMaxExportSize()` returns the maximum width or height in pixels. Both dimensions of every export must stay at or below this limit. `getAvailableMemory()` returns available memory in bytes, helping you assess whether large exports are feasible. > **Note:** The max export size is an upper bound. Exports may still fail due to memory constraints even when within size limits. For high-resolution exports, consider checking available memory first. Before an image export fails for memory, the engine frees decoded images from its caches and checks the memory again. The images that the GPU holds stay. `getAvailableMemory()` is unavailable on the iOS Simulator — guard the call with `try?` and treat the result as optional. ## API Reference | Method | Description | | ------------------------------------ | -------------------------------------------------------------------------------------------- | | `engine.block.export()` | Export a block with format and quality options. | | `engine.block.exportWithColorMask()` | Export a block with a specific RGB color removed, returning the masked image and alpha mask. | | `engine.block.exportVideo()` | Export a page as MP4 video with encoding options and progress events. | | `engine.block.exportAudio()` | Export audio from a page or audio block as WAV or AAC-encoded MP4. | | `engine.editor.getMaxExportSize()` | Get the maximum export dimension in pixels supported by the device. | | `engine.editor.getAvailableMemory()` | Get the currently available memory in bytes. | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Partial Export" description: "Export individual blocks, grouped elements, or specific pages from a CE.SDK scene in Swift instead of exporting the whole scene." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [Partial Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/) --- Export individual design elements, grouped blocks, or specific pages from your scene instead of exporting everything at once using CE.SDK's flexible export API. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-partial-export) Partial export gives you fine-grained control over what leaves the scene. Instead of rendering the entire composition, you can target a single graphic, a logical group of blocks, or one page out of a multi-page document. This is the foundation for asset-library generation, "export selection" features, page-by-page previews, and per-element output pipelines. ```swift file=@cesdk_swift_examples/engine-guides-partial-export/PartialExport.swift reference-only import Foundation import IMGLYEngine @MainActor func partialExport(engine: Engine) async throws { // Demo scaffolding: a two-page scene with three colored graphics on page 1 // and one graphic on page 2, so each highlighted snippet has real exportable // content. The rendered guide does not show this setup; readers start from a // scene already loaded into their app. let scene = try engine.scene.create() let page1 = try engine.block.create(.page) try engine.block.setWidth(page1, value: 800) try engine.block.setHeight(page1, value: 600) try engine.block.appendChild(to: scene, child: page1) let rectangle = try engine.block.create(.graphic) try engine.block.setShape(rectangle, shape: engine.block.createShape(.rect)) let rectangleFill = try engine.block.createFill(.color) try engine.block.setColor( rectangleFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0), ) try engine.block.setFill(rectangle, fill: rectangleFill) try engine.block.setPositionX(rectangle, value: 80) try engine.block.setPositionY(rectangle, value: 100) try engine.block.setWidth(rectangle, value: 220) try engine.block.setHeight(rectangle, value: 220) try engine.block.setName(rectangle, name: "background-rect") try engine.block.appendChild(to: page1, child: rectangle) let ellipse = try engine.block.create(.graphic) try engine.block.setShape(ellipse, shape: engine.block.createShape(.ellipse)) let ellipseFill = try engine.block.createFill(.color) try engine.block.setColor( ellipseFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.85, b: 0.2, a: 1.0), ) try engine.block.setFill(ellipse, fill: ellipseFill) try engine.block.setPositionX(ellipse, value: 340) try engine.block.setPositionY(ellipse, value: 100) try engine.block.setWidth(ellipse, value: 220) try engine.block.setHeight(ellipse, value: 220) try engine.block.appendChild(to: page1, child: ellipse) let star = try engine.block.create(.graphic) try engine.block.setShape(star, shape: engine.block.createShape(.star)) let starFill = try engine.block.createFill(.color) try engine.block.setColor( starFill, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.2, b: 0.2, a: 1.0), ) try engine.block.setFill(star, fill: starFill) try engine.block.setPositionX(star, value: 210) try engine.block.setPositionY(star, value: 350) try engine.block.setWidth(star, value: 220) try engine.block.setHeight(star, value: 220) try engine.block.appendChild(to: page1, child: star) let page2 = try engine.block.create(.page) try engine.block.setWidth(page2, value: 800) try engine.block.setHeight(page2, value: 600) try engine.block.appendChild(to: scene, child: page2) let page2Graphic = try engine.block.create(.graphic) try engine.block.setShape(page2Graphic, shape: engine.block.createShape(.rect)) let page2Fill = try engine.block.createFill(.color) try engine.block.setColor( page2Fill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.7, b: 0.4, a: 1.0), ) try engine.block.setFill(page2Graphic, fill: page2Fill) try engine.block.setPositionX(page2Graphic, value: 200) try engine.block.setPositionY(page2Graphic, value: 150) try engine.block.setWidth(page2Graphic, value: 400) try engine.block.setHeight(page2Graphic, value: 300) try engine.block.appendChild(to: page2, child: page2Graphic) let exportsDirectory = FileManager.default.temporaryDirectory let graphicBlocks = try engine.block.find(byType: .graphic) let namedBlocks = engine.block.find(byName: "background-rect") _ = namedBlocks guard let firstGraphic = graphicBlocks.first else { return } let pngOptions = ExportOptions(pngCompressionLevel: 5) let blockData = try await engine.block.export(firstGraphic, mimeType: .png, options: pngOptions) try blockData.write(to: exportsDirectory.appendingPathComponent("graphic.png")) let group = try engine.block.group([rectangle, ellipse]) let groupData = try await engine.block.export(group, mimeType: .png) try groupData.write(to: exportsDirectory.appendingPathComponent("group.png")) // In a real app the user makes the selection in the editor UI; here we set // it programmatically so findAllSelected returns a deterministic value. try engine.block.setSelected(star, selected: true) let selectedBlocks = engine.block.findAllSelected() if selectedBlocks.count == 1 { let selectionData = try await engine.block.export(selectedBlocks[0], mimeType: .png) try selectionData.write(to: exportsDirectory.appendingPathComponent("selection.png")) } else if selectedBlocks.count > 1 { let selectionGroup = try engine.block.group(selectedBlocks) let selectionData = try await engine.block.export(selectionGroup, mimeType: .png) try selectionData.write(to: exportsDirectory.appendingPathComponent("selection.png")) } if let currentPage = try engine.scene.getCurrentPage() { let pageData = try await engine.block.export(currentPage, mimeType: .png) try pageData.write(to: exportsDirectory.appendingPathComponent("current-page.png")) } let pages = try engine.scene.getPages() let pageStream = try await engine.block.export(pages, mimeType: .png) var pageIndex = 1 for try await data in pageStream { try data.write(to: exportsDirectory.appendingPathComponent("page-\(pageIndex).png")) pageIndex += 1 } let resizedOptions = ExportOptions(targetWidth: 1080, targetHeight: 1080) let resizedData = try await engine.block.export(page1, mimeType: .png, options: resizedOptions) try resizedData.write(to: exportsDirectory.appendingPathComponent("page-1080.png")) let jpegOptions = ExportOptions(jpegQuality: 0.8) let jpegData = try await engine.block.export(page1, mimeType: .jpeg, options: jpegOptions) try jpegData.write(to: exportsDirectory.appendingPathComponent("page.jpg")) let webpOptions = ExportOptions(webpQuality: 0.85) let webpData = try await engine.block.export(page1, mimeType: .webp, options: webpOptions) try webpData.write(to: exportsDirectory.appendingPathComponent("page.webp")) let maxExportSize = try engine.editor.getMaxExportSize() let availableMemory = try? engine.editor.getAvailableMemory() _ = maxExportSize _ = availableMemory let pdfOptions = ExportOptions(exportPdfWithHighCompatibility: true) let pdfData = try await engine.block.export(page1, mimeType: .pdf, options: pdfOptions) try pdfData.write(to: exportsDirectory.appendingPathComponent("page.pdf")) } ``` This guide covers exporting individual blocks, grouped elements, and pages with `engine.block.export(_:mimeType:options:)`, plus the format and sizing options that shape each output. ## Understanding Block Hierarchy and Export ### How Block Hierarchy Affects Exports CE.SDK organizes content as a tree: Scene → Pages → Groups → Individual Blocks. When you export a block, the export automatically includes every descendant of that block. Exporting a page exports every element on that page. Exporting a group exports the group and all of its children. Exporting an individual block (graphic, text, shape) exports only that block. The level of the hierarchy you target is what determines the scope of the output — choose the page for a complete layout, the group for a composite asset, or the block itself for a single element. ### Export Behavior The export pipeline normalizes a few things automatically. If the exported block itself is rotated, it is exported without that rotation so the content appears upright in the file. Any margin set on the block is included in the export bounds. Outside strokes are included for most block types; pages handle strokes differently. > **Note:** Only blocks that belong to the scene hierarchy can be exported. A block created with > `engine.block.create(_:)` but never appended to a parent already attached to the scene > cannot be exported until it is added to the tree. ## Exporting Individual Blocks ### Finding Blocks to Export Before exporting, locate the block. The most common entry points are `find(byType:)`, which returns every block of a given `DesignBlockType` (for example `.graphic`, `.text`, or `.page`), and `find(byName:)`, which returns blocks you have tagged with `engine.block.setName(_:name:)`. If the caller already holds a `DesignBlockID` reference (from a creation call or a tap handler), you can pass it directly. ```swift highlight-partialExport-findBlocks let graphicBlocks = try engine.block.find(byType: .graphic) let namedBlocks = engine.block.find(byName: "background-rect") ``` `find(byType: .graphic)` returns every graphic block in the scene regardless of fill content — a graphic with a solid color fill, an image fill, and a gradient fill are all returned. Filter further by inspecting the block's fill or kind if you need a specific subset. ### Basic Block Export `engine.block.export(_:mimeType:options:)` is `async throws` and returns `Blob`, which is a typealias for `Data`. Pass the block's `DesignBlockID`, the desired `MIMEType`, and an `ExportOptions` configured for that format. Persist the returned `Data` with `Data.write(to:)` against any writable URL — `FileManager.default.temporaryDirectory` works well for ephemeral output that is later uploaded or shared. ```swift highlight-partialExport-exportIndividualBlock let pngOptions = ExportOptions(pngCompressionLevel: 5) let blockData = try await engine.block.export(firstGraphic, mimeType: .png, options: pngOptions) try blockData.write(to: exportsDirectory.appendingPathComponent("graphic.png")) ``` CE.SDK supports `MIMEType.png`, `.jpeg`, `.webp`, and `.pdf` for static partial exports. Each format reads a different option from `ExportOptions` — PNG uses `pngCompressionLevel`, JPEG uses `jpegQuality`, WEBP uses `webpQuality`, and PDF uses `exportPdfWithHighCompatibility`. The [Quality and Compression](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/#quality-and-compression) section shows a JPEG/WEBP example and the [Export Limitations](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/#export-limitations-and-considerations) section shows a PDF example; other fields are ignored for formats that do not consume them. PNG is ideal for graphics that need transparency such as UI elements, logos, or illustrations with alpha channels. JPEG produces smaller files for photographs but drops transparency, replacing it with a solid background. WEBP delivers better compression than PNG or JPEG for web pipelines. PDF preserves vector information for print workflows. ## Exporting Grouped Elements ### Creating and Exporting Groups Groups are useful when several blocks should leave the scene as a single output — a logo with multiple components, a composite illustration, or any layout section that should not be split. `engine.block.group(_:)` takes an array of `DesignBlockID` values, returns the new group's ID, and you export that ID like any other block. ```swift highlight-partialExport-createAndExportGroup let group = try engine.block.group([rectangle, ellipse]) let groupData = try await engine.block.export(group, mimeType: .png) try groupData.write(to: exportsDirectory.appendingPathComponent("group.png")) ``` When a group is exported, CE.SDK renders all children together into one file. The group's bounding box determines the export dimensions, and the relative positioning of children is preserved exactly as designed. ### Exporting Selected Elements A common product workflow is to export whatever the user has selected. `engine.block.findAllSelected()` returns the currently selected `DesignBlockID`s; if there is exactly one, export it directly, and if there are several, group them temporarily and export the group. This lets a single "Export Selection" action handle both cases without branching in the UI layer. ```swift highlight-partialExport-exportSelected let selectedBlocks = engine.block.findAllSelected() if selectedBlocks.count == 1 { let selectionData = try await engine.block.export(selectedBlocks[0], mimeType: .png) try selectionData.write(to: exportsDirectory.appendingPathComponent("selection.png")) } else if selectedBlocks.count > 1 { let selectionGroup = try engine.block.group(selectedBlocks) let selectionData = try await engine.block.export(selectionGroup, mimeType: .png) try selectionData.write(to: exportsDirectory.appendingPathComponent("selection.png")) } ``` In an editor app the user provides the selection through the canvas. The Swift sample above sets the selection programmatically with `engine.block.setSelected(_:selected:)` so the snippet is reproducible in a test or on a freshly loaded scene. ## Exporting Pages `engine.scene.getCurrentPage()` returns the active page as a `DesignBlockID?` — it is optional because the scene may not yet have a current page. Use `if let` to unwrap it before exporting. To produce previews for every page in a multi-page document, walk `engine.scene.getPages()` instead. ```swift highlight-partialExport-exportCurrentPage if let currentPage = try engine.scene.getCurrentPage() { let pageData = try await engine.block.export(currentPage, mimeType: .png) try pageData.write(to: exportsDirectory.appendingPathComponent("current-page.png")) } ``` Page exports include the page background, every element on the page, and any page-level effects. The page's own dimensions become the output dimensions unless `targetWidth`/`targetHeight` override them. For multi-page documents, `engine.block.export(_:mimeType:options:)` accepts an array of IDs and returns an `AsyncThrowingStream` that yields one blob per page in input order. The streaming variant reuses a single background worker across the batch, so it is more memory-efficient than calling `export` in a loop. ```swift highlight-partialExport-exportAllPages let pages = try engine.scene.getPages() let pageStream = try await engine.block.export(pages, mimeType: .png) var pageIndex = 1 for try await data in pageStream { try data.write(to: exportsDirectory.appendingPathComponent("page-\(pageIndex).png")) pageIndex += 1 } ``` Use sequential numbering when writing the blobs so files are easy to recombine. PDF is a good choice when downstream consumers expect one document with selectable text; PNG or WEBP suits image previews and per-page thumbnails. ## Export Options and Configuration ### Target Size Control `ExportOptions(targetWidth:targetHeight:)` requests output at a specific size while preserving the block's aspect ratio. The block is rendered large enough to fill the target completely; if the requested size has a different aspect than the block, one axis may extend past the target so proportions stay correct. There is no stretching or distortion. ```swift highlight-partialExport-targetSize let resizedOptions = ExportOptions(targetWidth: 1080, targetHeight: 1080) let resizedData = try await engine.block.export(page1, mimeType: .png, options: resizedOptions) try resizedData.write(to: exportsDirectory.appendingPathComponent("page-1080.png")) ``` `targetWidth` and `targetHeight` are pixel values regardless of the scene's design unit, which makes them well-suited for responsive thumbnails, social-media presets, and platform-imposed dimensions. ### Quality and Compression Each lossy format exposes its own quality knob on `ExportOptions`. Use them to trade output size against visual fidelity. ```swift highlight-partialExport-qualityOptions let jpegOptions = ExportOptions(jpegQuality: 0.8) let jpegData = try await engine.block.export(page1, mimeType: .jpeg, options: jpegOptions) try jpegData.write(to: exportsDirectory.appendingPathComponent("page.jpg")) let webpOptions = ExportOptions(webpQuality: 0.85) let webpData = try await engine.block.export(page1, mimeType: .webp, options: webpOptions) try webpData.write(to: exportsDirectory.appendingPathComponent("page.webp")) ``` | Field | Range | Behavior | | --- | --- | --- | | `pngCompressionLevel` | `0`–`9` (default `5`) | Higher values produce smaller files at the cost of encoding time. PNG is lossless, so quality is unaffected. | | `jpegQuality` | `(0, 1]` (default `0.9`) | Higher values produce larger, sharper files. Values above `0.9` are visually transparent for most content. | | `webpQuality` | `(0, 1]` (default `1.0`) | A value of `1.0` triggers WEBP's lossless mode, which often beats PNG on file size for the same content. | ### Export Size Limits Before requesting a very large export, query the device's reported limits. `getMaxExportSize()` returns the maximum dimension in pixels (returning `Int32.max` when the limit is unknown). Both width and height of the export must stay below or equal to this value. `getAvailableMemory()` returns free engine memory in bytes; an export that fits within `getMaxExportSize()` may still fail if memory is tight. ```swift highlight-partialExport-checkLimits let maxExportSize = try engine.editor.getMaxExportSize() let availableMemory = try? engine.editor.getAvailableMemory() ``` `getAvailableMemory()` is unavailable on the iOS Simulator and returns `nil` there. Call it with `try?` and treat `nil` as "unknown memory" rather than failing the workflow — on real devices it returns a byte count you can use to size exports. Use the values to gate user-facing presets, warn on requests that exceed the device limit, or pick a smaller `targetWidth`/`targetHeight` automatically when memory is constrained. ## Export Limitations and Considerations ### Format-Specific Constraints JPEG drops transparency from the output, replacing transparent pixels with a solid background. Designs that rely on alpha channels should export to PNG or WEBP instead. PDF behavior depends on `ExportOptions.exportPdfWithHighCompatibility`. With `true` (the default), bitmap content and effects are rasterized at the scene DPI for broader viewer compatibility. With `false`, PDFs export faster by embedding images directly, but gradients with transparency may not render correctly in Safari or macOS Preview. See the [Export to PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) guide for detailed performance guidance. ```swift highlight-partialExport-exportPDF let pdfOptions = ExportOptions(exportPdfWithHighCompatibility: true) let pdfData = try await engine.block.export(page1, mimeType: .pdf, options: pdfOptions) try pdfData.write(to: exportsDirectory.appendingPathComponent("page.pdf")) ``` ### Performance Considerations `engine.block.export(_:mimeType:options:)` runs on a background worker engine and is `async`, so it does not block the UI thread, but the call still takes time proportional to the rendered area. Show a progress indicator for large exports, and prefer the array-of-IDs overload when batching — it reuses a single worker across the batch instead of creating a new one per export. On iOS, the worker is automatically suspended when the app moves to the background and resumed when it returns to the foreground. This affects long-running exports such as video, but static partial exports usually finish before suspension matters. ### Hierarchy Requirements Only blocks attached to the scene can be exported. Always append blocks to a page (or to a parent that is itself in the tree) before calling `export`. Graphic blocks additionally need both a shape and a fill set — `engine.block.create(.graphic)` produces an empty placeholder until `setShape(_:shape:)` and `setFill(_:fill:)` are applied. ## API Reference | Method | Description | | --- | --- | | `engine.block.export(_:mimeType:options:)` | Exports a single block as `Data`. `async throws`. | | `engine.block.export(_:mimeType:options:)` (array) | Exports an array of blocks as an `AsyncThrowingStream` that yields one blob per ID. | | `engine.block.find(byType:)` | Returns every block matching a `DesignBlockType`, `FillType`, `ShapeType`, `EffectType`, or `BlurType`. | | `engine.block.find(byName:)` | Returns blocks tagged with `setName(_:name:)`. | | `engine.block.findAllSelected()` | Returns the currently selected blocks. | | `engine.block.group(_:)` | Groups multiple blocks under a single new parent and returns its ID. | | `engine.scene.getCurrentPage()` | Returns the active page as `DesignBlockID?`. | | `engine.scene.getPages()` | Returns every page in scene order. | | `engine.editor.getMaxExportSize()` | Returns the device's maximum export dimension in pixels. | | `engine.editor.getAvailableMemory()` | Returns free engine memory in bytes. Unavailable on the iOS Simulator — call with `try?` and treat `nil` as "unknown". | | `ExportOptions` | Per-format options: `pngCompressionLevel`, `jpegQuality`, `webpQuality`, `targetWidth`, `targetHeight`, `exportPdfWithHighCompatibility`. | ## Next Steps - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Fundamentals of exporting from CE.SDK - [Export to PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) — Multi-page PDF output and print-ready settings - [Export to JPEG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-jpeg-6f88e9/) — JPEG quality and color handling - [Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/) — Tune `maxImageSize` and validate exports against device capabilities --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Size Limits" description: "Configure and understand CE.SDK's image and video size limits in Swift to balance quality and performance across devices." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/) --- Configure size limits to balance quality and performance in CE.SDK applications. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-size-limits) CE.SDK processes images and videos on the device, so size limits depend on the available memory and the device's rendering hardware. Tuning these limits keeps memory use predictable on smaller devices while still letting capable devices export at high resolution. ```swift file=@cesdk_swift_examples/engine-guides-size-limits/SizeLimits.swift reference-only import Foundation import IMGLYEngine @MainActor func sizeLimits(engine: Engine) async throws { try engine.scene.create() // Use pixels as the scene's design unit so block dimensions can be compared // directly to pixel-based limits like getMaxExportSize(). try engine.scene.setDesignUnit(.px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) if let scene = try engine.scene.get() { try engine.block.appendChild(to: scene, child: page) } let currentMaxImageSize = try engine.editor.getSettingInt("maxImageSize") // The default value is 4096 pixels. // Lower the limit on memory-constrained devices. Apply this before loading // images so newly loaded textures are downscaled to the new limit. try engine.editor.setSettingInt("maxImageSize", value: 2048) // Or raise it for high-quality workflows on capable devices: // try engine.editor.setSettingInt("maxImageSize", value: 8192) // Observe settings changes via an AsyncStream and react to new values. Cancel // the task to unsubscribe. let observation = Task { for await _ in engine.editor.onSettingsChanged { let newMaxImageSize = try engine.editor.getSettingInt("maxImageSize") _ = newMaxImageSize } } // ... observation.cancel() // The engine reports the maximum export size supported on the current device. // The value is an upper bound — exports may still fail for memory or other // reasons. When the limit is unknown, the engine returns Int32.max. let maxExportSize = try engine.editor.getMaxExportSize() // getWidth/getHeight only return absolute pixel values when the scene's // design unit is .px AND the block's size mode is .absolute. With .percent // the value is a fraction of the parent's size; with .auto it is derived // from the block's content. Check both before comparing to the pixel-based // device limit. let designUnit = try engine.scene.getDesignUnit() let widthMode = try engine.block.getWidthMode(page) let heightMode = try engine.block.getHeightMode(page) if designUnit == .px, widthMode == .absolute, heightMode == .absolute { let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) let withinLimit = Int(pageWidth.rounded(.up)) <= maxExportSize && Int(pageHeight.rounded(.up)) <= maxExportSize _ = withinLimit } // Catch export errors so the app can recover. Common remediations are // lowering targetWidth/targetHeight or reducing maxImageSize. // ExportOptions.targetWidth/targetHeight are always in pixels. do { let pngData = try await engine.block.export(page, mimeType: .png) _ = pngData } catch { try engine.editor.setSettingInt("maxImageSize", value: 2048) let retryOptions = ExportOptions(targetWidth: 1920, targetHeight: 1080) let retryData = try await engine.block.export(page, mimeType: .png, options: retryOptions) _ = retryData } } ``` This guide covers reading and writing the `maxImageSize` setting, observing setting changes, querying the device's maximum export size, and handling export failures. ## Understanding Size Limits CE.SDK manages size limits at two stages: **input** (when loading images) and **output** (when exporting). The `maxImageSize` setting controls input resolution and downscales images that exceed the configured limit before they reach the canvas. The default is 4096×4096 pixels, which keeps memory use predictable on a wide range of devices. Export resolution has no artificial limit. The engine can render up to 16,384×16,384 pixels in theory, but the actual ceiling is determined by the device's rendering hardware and available memory. Use `engine.editor.getMaxExportSize()` to read the device's reported upper bound at runtime. ## Resolution & Duration Limits | Constraint | Recommendation / Limit | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Input Resolution** | Maximum input resolution is **4096×4096 pixels**. Images from external sources (e.g., Unsplash) are resized to this size before rendering on the canvas. You can modify this value using the `maxImageSize` setting. | | **Output Resolution** | There is no enforced output resolution limit. Theoretically, the editor supports output sizes up to **16,384×16,384 pixels**. However, practical limits depend on the device's GPU capabilities and available memory. | All image processing in CE.SDK happens on the device running the engine, so these values depend on the **maximum texture size** supported by its hardware. The default limit of 4096×4096 is a safe baseline that works universally. Higher resolutions (e.g., 8192×8192) may work on certain devices but could fail on others during export if the GPU texture size is exceeded. > **Note:** To ensure consistent results across devices, it’s best to test higher output > sizes on your target hardware and set conservative defaults in production. | Constraint | Recommendation / Limit | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Resolution** | Up to **4K UHD** is supported for **playback** and **export** on capable devices, bounded by GPU texture size and available memory. For **import**, CE.SDK does not impose artificial limits, but maximum video size is bounded by available device memory and the native media framework's decoder capabilities. The maximum export dimension varies by device and can be queried via `engine.editor.getMaxExportSize()`. | | **Frame Rate** | 30 FPS at 1080p is broadly supported; 60 FPS and high-resolution exports benefit from hardware acceleration via the device's native media frameworks. | | **Duration** | Stories and reels of up to **2 minutes** are fully supported. Longer videos are also supported, but we generally found a maximum duration of **10 minutes** to be a good balance for a smooth editing experience and a pleasant export duration of around one minute on modern hardware. | > **Note:** Performance scales with device hardware. For best results with high-resolution > or high-frame-rate video, modern devices with hardware video acceleration are > recommended. ## Configuring maxImageSize Read and modify `maxImageSize` through the Settings API. The setting is an integer (pixels), so use the `Int` accessors on `engine.editor`. ### Reading the Current Setting To check the value currently in effect: ```swift highlight-sizeLimits-readSetting let currentMaxImageSize = try engine.editor.getSettingInt("maxImageSize") // The default value is 4096 pixels. ``` The default is `4096`. Read this value at startup to surface it in your UI, or to make runtime decisions about asset loading. ### Setting a New Value Apply a new limit before loading images so newly loaded textures are downscaled to the new size: ```swift highlight-sizeLimits-writeSetting // Lower the limit on memory-constrained devices. Apply this before loading // images so newly loaded textures are downscaled to the new limit. try engine.editor.setSettingInt("maxImageSize", value: 2048) // Or raise it for high-quality workflows on capable devices: // try engine.editor.setSettingInt("maxImageSize", value: 8192) ``` Images already on the canvas keep their loaded resolution until they are reloaded. Lower values reduce memory pressure on phones and tablets; higher values preserve detail on desktops and higher-end devices. ### Observing Settings Changes Subscribe to settings changes through the `onSettingsChanged` async stream. The stream emits `Void` on every setting change, so read the value back inside the loop: ```swift highlight-sizeLimits-observeChanges // Observe settings changes via an AsyncStream and react to new values. Cancel // the task to unsubscribe. let observation = Task { for await _ in engine.editor.onSettingsChanged { let newMaxImageSize = try engine.editor.getSettingInt("maxImageSize") _ = newMaxImageSize } } // ... observation.cancel() ``` A Combine variant is also available as `engine.editor.onSettingsChangedPublisher`. Cancel the consuming `Task` (or the Combine subscription) to unsubscribe. ## Device Export Capabilities The maximum export size on the current device is exposed directly: ```swift highlight-sizeLimits-maxExportSize // The engine reports the maximum export size supported on the current device. // The value is an upper bound — exports may still fail for memory or other // reasons. When the limit is unknown, the engine returns Int32.max. let maxExportSize = try engine.editor.getMaxExportSize() ``` `getMaxExportSize()` returns the upper export limit in pixels for both width and height. When the limit is unknown the engine returns `Int32.max` to signal "unlimited". The reported value is an upper bound: exports may still fail for memory reasons even when both dimensions are below it. Use the value to: - Cap export presets to dimensions the device can render - Warn users when a requested export exceeds the device limit - Pick a conservative default `maxImageSize` for the device class You can also pre-validate a planned export against the limit. `getWidth(_:)` and `getHeight(_:)` only return absolute pixel values when the scene's design unit is `.px` **and** the block's size mode is `.absolute`. With `.percent` the value is a fraction of the parent's size; with `.auto` it is derived from the block's content. Verify both before comparing to `getMaxExportSize()`: ```swift highlight-sizeLimits-validateExport // getWidth/getHeight only return absolute pixel values when the scene's // design unit is .px AND the block's size mode is .absolute. With .percent // the value is a fraction of the parent's size; with .auto it is derived // from the block's content. Check both before comparing to the pixel-based // device limit. let designUnit = try engine.scene.getDesignUnit() let widthMode = try engine.block.getWidthMode(page) let heightMode = try engine.block.getHeightMode(page) if designUnit == .px, widthMode == .absolute, heightMode == .absolute { let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) let withinLimit = Int(pageWidth.rounded(.up)) <= maxExportSize && Int(pageHeight.rounded(.up)) <= maxExportSize _ = withinLimit } ``` ## Handling Export Errors `engine.block.export(_:mimeType:options:)` is `async throws`, so wrap it in a `do/catch` block and provide a fallback when an export fails. A practical recovery is to lower `maxImageSize`, then retry with smaller `targetWidth`/`targetHeight` values: ```swift highlight-sizeLimits-handleExport // Catch export errors so the app can recover. Common remediations are // lowering targetWidth/targetHeight or reducing maxImageSize. // ExportOptions.targetWidth/targetHeight are always in pixels. do { let pngData = try await engine.block.export(page, mimeType: .png) _ = pngData } catch { try engine.editor.setSettingInt("maxImageSize", value: 2048) let retryOptions = ExportOptions(targetWidth: 1920, targetHeight: 1080) let retryData = try await engine.block.export(page, mimeType: .png, options: retryOptions) _ = retryData } ``` This pattern lets the app keep delivering an export even when the first attempt is too large for the current device or memory pressure is high. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Images appear blurry on the canvas | `maxImageSize` is below the source resolution | Raise `maxImageSize` if the device has the memory headroom | | Out-of-memory crashes during editing | `maxImageSize` is too high for the device | Lower `maxImageSize`, especially on phones and tablets | | Export throws unexpectedly | Output dimensions exceed `getMaxExportSize()` | Reduce `targetWidth`/`targetHeight` or pick a smaller export preset | | Video export fails | Resolution or duration exceeds device capability | Export at 1080p instead of 4K, or shorten the video | | Inconsistent results across devices | Different rendering hardware | Set a conservative `maxImageSize` (4096) and gate larger exports on `getMaxExportSize()` | ## API Reference | Method | Description | | --- | --- | | `engine.editor.getSettingInt(_:)` | Reads an integer setting (e.g. `maxImageSize`) | | `engine.editor.setSettingInt(_:value:)` | Updates an integer setting | | `engine.editor.onSettingsChanged` | `AsyncStream` that emits when any setting changes | | `engine.editor.getMaxExportSize()` | Returns the device's maximum export dimension in pixels | | `engine.block.export(_:mimeType:options:)` | Exports a block as image data | | `engine.block.getWidth(_:)` / `getHeight(_:)` | Returns block dimensions in the scene's design unit. Values are absolute only when the size mode is `.absolute`; `.percent` returns a parent-relative fraction and `.auto` returns a content-derived value | | `engine.block.getWidthMode(_:)` / `getHeightMode(_:)` | Returns the size mode (`.absolute`, `.percent`, or `.auto`) used for the dimension | | `engine.scene.getDesignUnit()` / `setDesignUnit(_:)` | Reads or sets the scene's design unit | ## Next Steps Explore related guides to build complete export workflows: - [Settings Guide](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) - Complete Settings API reference and configuration options - [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/file-format-support-3c4b2a/) - Supported image and video formats with capabilities - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) - Fundamentals of exporting images and videos from CE.SDK - [Export to PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) - PDF export guide with multi-page support and print optimization --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To JPEG" description: "Export CE.SDK designs to JPEG format with configurable quality settings for photographs, web images, and social media content." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-jpeg-6f88e9/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [To JPEG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-jpeg-6f88e9/) --- Export CE.SDK designs to JPEG format—ideal for photographs, social media, and web content where file size matters more than transparency. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-export-to-jpeg) JPEG uses lossy compression optimized for photographs and smooth color gradients. Unlike PNG, JPEG does not support transparency—transparent areas render with a solid background. ```swift file=@cesdk_swift_examples/engine-guides-export-to-jpeg/ToJpeg.swift reference-only import Foundation import IMGLYEngine @MainActor func toJpeg(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL try engine.editor.setSettingString("basePath", value: baseURL.absoluteString) let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: sceneURL) let page = try engine.scene.getPages().first! let blob: Blob = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.9), ) let highQualityBlob = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 1.0), ) let sizedBlob = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions( jpegQuality: 0.85, targetWidth: 1920, targetHeight: 1080, ), ) let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("export.jpg") try blob.write(to: outputURL) _ = highQualityBlob _ = sizedBlob } ``` This guide covers exporting to JPEG, configuring quality and dimensions, and saving exports to disk. ## Export to JPEG Export a design block by calling `engine.block.export(_:mimeType:options:)` with `.jpeg` as the MIME type. The call returns a `Blob` (a `Data` value) containing the encoded image. ```swift highlight-toJpeg-exportJpeg let blob: Blob = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 0.9), ) ``` The `jpegQuality` parameter accepts values from greater than 0 to 1. Higher values produce better quality at larger file sizes. The default is `0.9`. ## Export Options JPEG export reads these fields from `ExportOptions`: | Option | Type | Default | Description | | -------------- | ------- | ------- | ------------------------------------------------------------------------------------ | | `jpegQuality` | `Float` | `0.9` | Quality from >0 to 1 | | `targetWidth` | `Float` | `0` | Output width in pixels. Used together with `targetHeight`; `0` disables the override | | `targetHeight` | `Float` | `0` | Output height in pixels. Used together with `targetWidth`; `0` disables the override | ### Quality Control Set `jpegQuality` to `1.0` for maximum quality with minimal compression artifacts. This is useful for archival or print preparation. ```swift highlight-toJpeg-exportQuality let highQualityBlob = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions(jpegQuality: 1.0), ) ``` For web delivery, values around `0.8` balance quality and file size effectively. ### Target Dimensions Specify `targetWidth` and `targetHeight` to export at exact dimensions. The output fills the target size while maintaining aspect ratio. ```swift highlight-toJpeg-exportSize let sizedBlob = try await engine.block.export( page, mimeType: .jpeg, options: ExportOptions( jpegQuality: 0.85, targetWidth: 1920, targetHeight: 1080, ), ) ``` ## Save to File System The returned `Blob` is a `Data` value, so writing it to disk is a single call to `write(to:)`. ```swift highlight-toJpeg-saveFile let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("export.jpg") try blob.write(to: outputURL) ``` ## When to Use JPEG JPEG works well for: - Photographs and images with gradual color transitions - Social media posts and web content - Scenarios where file size matters more than perfect quality > **Note:** For graphics with sharp edges, text, or transparency, use PNG instead. For modern web delivery with better compression, consider WebP. ## Troubleshooting **Output looks blurry** — Increase `jpegQuality` toward `1.0`, or use PNG for graphics with hard edges. **File size too large** — Decrease `jpegQuality` to `0.7`–`0.8`, or reduce dimensions with `targetWidth` and `targetHeight`. **Unexpected background** — JPEG does not support transparency. Use PNG or WebP for transparent content. ## API Reference | Method | Description | | ------------------------------------------ | ---------------------------------------------------------------------------------------- | | `engine.block.export(_:mimeType:options:)` | Export a block to the specified format | | `engine.scene.load(from:)` | Load a scene from a remote URL | | `engine.scene.getPages()` | Return all pages in the current scene | | `ExportOptions` | Format-specific export configuration; JPEG reads `jpegQuality`, `targetWidth`, `targetHeight` | ## Next Steps - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Compare all available export formats - [Export to PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) — Export for print and document workflows - [Partial Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/) — Learn how to export specific blocks, groups, and page elements instead of entire scenes using CE.SDK's programmatic export API. - [Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/) — Understand and configure limits on exported file dimensions or data size. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To MP4" description: "Export video compositions as MP4 files with H.264 encoding, progress events, and configurable quality and resolution." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-mp4-c998a8/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [To MP4](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-mp4-c998a8/) --- Export your video compositions as MP4 files with H.264 encoding, progress events, and configurable quality and resolution. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-export-to-mp4) MP4 is the most widely supported video format, using H.264 encoding for efficient compression. CE.SDK renders frames, encodes them with H.264, and muxes audio into an MP4 container. The export runs on the engine's background worker so the main thread stays responsive. ```swift file=@cesdk_swift_examples/engine-guides-export-to-mp4/ExportToMp4.swift reference-only import Foundation import IMGLYEngine // swiftlint:disable cyclomatic_complexity @MainActor func exportToMp4(engine: Engine) async throws { // Demo scaffolding: build a video scene with a single page and a video fill so // the exportVideo calls below have something to encode. In your app you would // start from a scene already loaded into the editor instead. let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 5) let baseURL = try engine.guidesBaseURL let video = try engine.block.create(.graphic) try engine.block.setShape(video, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) try engine.block.setURL( videoFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) try engine.block.setFill(video, fill: videoFill) try engine.block.appendChild(to: page, child: video) try engine.block.fillParent(video) let exportsDirectory = FileManager.default.temporaryDirectory let videoStream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in videoStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video.mp4")) } } let progressStream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in progressStream { switch event { case let .progress(rendered, encoded, total): let percent = total > 0 ? Int(Double(encoded) / Double(total) * 100) : 0 print("Export \(percent)% — encoded \(encoded)/\(total) (rendered \(rendered))") case let .finished(video: blob): try blob.write(to: exportsDirectory.appendingPathComponent("progress.mp4")) } } let exportTask = Task { () -> Blob in let stream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in stream { try Task.checkCancellation() if case let .finished(video: blob) = event { return blob } } throw CancellationError() } // Call exportTask.cancel() from another task to abort the export. let exportedBlob = try await exportTask.value try exportedBlob.write(to: exportsDirectory.appendingPathComponent("cancellable.mp4")) let resolutionOptions = VideoExportOptions( framerate: 30, targetWidth: 1920, targetHeight: 1080, ) let resolutionStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: resolutionOptions) for try await event in resolutionStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-1080p.mp4")) } } let qualityOptions = VideoExportOptions( h264Profile: .high, h264Level: 52, videoBitrate: 8_000_000, ) let qualityStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: qualityOptions) for try await event in qualityStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-high.mp4")) } } // Override current playback with a cover from the exported video start. let coverOptions = VideoExportOptions(coverTimeOffset: 0) let coverStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: coverOptions) for try await event in coverStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-cover.mp4")) } } let partialOptions = VideoExportOptions( timeOffset: 1, duration: 2, ) let partialStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: partialOptions) for try await event in partialStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-clip.mp4")) } } } // swiftlint:enable cyclomatic_complexity ``` This guide covers exporting a page to MP4, observing progress, cancelling an in-flight export, configuring resolution and quality, and exporting a partial timeline range. ## Export to MP4 Call `engine.block.exportVideo(_:mimeType:options:)` with a page block to export it as an MP4 video. The call returns an `AsyncThrowingStream` that yields `.progress` events while encoding and a final `.finished(video:)` event that carries the encoded `Blob`. ```swift highlight-exportToMp4-exportVideo let videoStream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in videoStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video.mp4")) } } ``` `MIMEType.mp4` is the default — pass it explicitly to make the output format obvious to readers of the call site. Once the stream finishes, write the resulting `Blob` to disk with `Blob.write(to:)`. > **Caution:** H.264 does not support transparency. Transparent areas in your scene render with a black background in the exported MP4. ## Tracking Export Progress `.progress` events arrive throughout the export with three counters: rendered frames (frames pulled from the scene), encoded frames (frames already pushed through the H.264 encoder), and the total frame count. The encoded count is the most useful signal for a user-facing progress bar because it tracks the slower stage. ```swift highlight-exportToMp4-progress let progressStream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in progressStream { switch event { case let .progress(rendered, encoded, total): let percent = total > 0 ? Int(Double(encoded) / Double(total) * 100) : 0 print("Export \(percent)% — encoded \(encoded)/\(total) (rendered \(rendered))") case let .finished(video: blob): try blob.write(to: exportsDirectory.appendingPathComponent("progress.mp4")) } } ``` > **Note:** On iPhone and iPad apps, the export process is automatically suspended when the app moves to the background and resumed when it returns to the foreground. You don't need to handle lifecycle events manually. This automatic handling does not apply to Mac Catalyst or macOS targets. ## Cancelling an Export Wrap the export in a `Task` so a holder can call `cancel()` to abort it. Each loop iteration calls `try Task.checkCancellation()` first, so the loop exits as soon as cancellation is requested and the engine tears down the worker. ```swift highlight-exportToMp4-cancel let exportTask = Task { () -> Blob in let stream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in stream { try Task.checkCancellation() if case let .finished(video: blob) = event { return blob } } throw CancellationError() } // Call exportTask.cancel() from another task to abort the export. let exportedBlob = try await exportTask.value try exportedBlob.write(to: exportsDirectory.appendingPathComponent("cancellable.mp4")) ``` Hold a reference to the `Task` value and call `cancel()` from your UI (for example, when a user taps a Cancel button). The `try Task.checkCancellation()` call surfaces a `CancellationError` through `exportTask.value` instead of returning a partial result. The export runs on a worker engine of its own, so releasing your engine or destroying the page does not stop it. Cancel the task, or end the loop over the stream. The export then stops at once, also while the app is in the background, and the worker engine releases its memory. ## Configure Video Encoding Pass a `VideoExportOptions` value to control quality, file size, and device compatibility. ### Resolution and Framerate Set `targetWidth`, `targetHeight`, and `framerate` to control output dimensions and smoothness. ```swift highlight-exportToMp4-resolution let resolutionOptions = VideoExportOptions( framerate: 30, targetWidth: 1920, targetHeight: 1080, ) let resolutionStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: resolutionOptions) for try await event in resolutionStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-1080p.mp4")) } } ``` If only one of `targetWidth` or `targetHeight` is non-zero, the other is computed to preserve the source aspect ratio. Without `framerate`, the video uses the scene's `scene/frameRate` (30 by default). ### H.264 Profile and Quality The `h264Profile` option controls encoding quality and device compatibility: - **`.baseline` (66)**: Maximum compatibility, lower compression. - **`.main` (77)**: Balanced quality and compatibility (default). - **`.extended` (88)**: Extended feature set, less common. - **`.high` (100)**: Best compression, supported on all modern iOS devices. `h264Level` accepts the level multiplied by ten — pass `52` for level 5.2. `videoBitrate` accepts an explicit value in bits per second, or one of the named modes: `VideoBitrate.system` (`0`, the default) lets the platform encoder (VideoToolbox) choose, and `VideoBitrate.auto` (`-1`) picks a bounded value based on resolution and framerate. ```swift highlight-exportToMp4-quality let qualityOptions = VideoExportOptions( h264Profile: .high, h264Level: 52, videoBitrate: 8_000_000, ) let qualityStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: qualityOptions) for try await event in qualityStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-high.mp4")) } } ``` The example above sets `videoBitrate: 8_000_000` (8 Mbps), a reasonable target for high-quality 1080p H.264 footage. As a rough guide, scale the bitrate with the pixel count and motion complexity: ~5 Mbps for 720p, 8–12 Mbps for 1080p, and 20–40 Mbps for 4K. ### Export a Partial Timeline Use `timeOffset` and `duration` to export a specific segment without modifying the scene. Both values are in seconds, relative to the page's timeline. ```swift highlight-exportToMp4-partial let partialOptions = VideoExportOptions( timeOffset: 1, duration: 2, ) let partialStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: partialOptions) for try await event in partialStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-clip.mp4")) } } ``` A `duration` of `0` defaults to the full duration of the exported page (the engine does not subtract `timeOffset`). ### All MP4 Export Options | Option | Type | Default | Description | | ------------------- | ------------- | ------------ | ---------------------------------------------------------------------------------------------------- | | `h264Profile` | `H264Profile` | `.main` | Encoder profile: `.baseline` (66), `.main` (77), `.extended` (88), `.high` (100). | | `h264Level` | `Int32` | `52` | Encoding level (multiply desired level by 10, e.g., `52` for level 5.2). | | `videoBitrate` | `Int32` | `VideoBitrate.system` | Video bitrate in bits/second, or a named mode. `VideoBitrate.system` (`0`, the default) lets VideoToolbox choose; `VideoBitrate.auto` (`-1`) picks a bounded, resolution-aware bitrate. A positive number sets an explicit bitrate (maximum determined by profile and level). | | `audioBitrate` | `Int32` | `0` (auto) | Audio bitrate in bits/second. `0` defaults to 128 kbps for stereo AAC. | | `framerate` | `Float?` | `nil` | Target framerate in Hz, greater than 0 and at most 240. `nil` uses the scene's `scene/frameRate`. | | `targetWidth` | `Float` | `0` | Output width in pixels. `0` keeps the page's natural width. | | `targetHeight` | `Float` | `0` | Output height in pixels. `0` keeps the page's natural height. | | `timeOffset` | `Double` | `0` | Start time offset in seconds. | | `duration` | `Double` | page length | Video duration in seconds. `0` defaults to the duration of the exported page. | | `allowTextOverhang` | `Bool` | `false` | Include text bounding boxes that account for glyph overhangs. | | `videoCodec` | `VideoCodec` | `.auto` | `.h264`, `.hevc`, `.vp8`, `.vp9` or `.av1`. `.auto` picks H.264 for `.mp4` and VP9 for `.webm`. A codec that the file type cannot hold fails the export. | | `audioCodec` | `AudioCodec` | `.auto` | `.aac`, `.opus` or `.vorbis`. `.auto` picks AAC for `.mp4` and Opus for `.webm`. | | `exportAlpha` | `Bool` | `false` | Keeps the transparency of the page. Only `.vp8` and `.vp9` with `MIMEType.webm`. | ## API Reference | Method | Description | | ----------------------------------------------- | -------------------------------------------------------------------------------------------- | | `engine.block.exportVideo(_:mimeType:options:)` | Export a page block as MP4 video. Returns an `AsyncThrowingStream`. | ## Next Steps - [Export to WebM](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-webm-764271/) - Export with the royalty-free VP8, VP9 or AV1 codecs - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) - Compare all supported export formats - [Export Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/) - Check device limits before exporting large videos - [Export Audio](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/audio-68de25/) - Export audio tracks separately - [Partial Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/) - Export specific blocks or timeline segments ## Set a Video Cover By default, MP4 exports include a cover from the page playback position when export starts. Omit `coverTimeOffset` or pass `nil` to use this position. For a partial export, playback stays relative to the page timeline. A position outside the exported segment uses its first or last frame. Set `coverTimeOffset` to override the default in seconds from the beginning of the exported video. It must be finite, at least `0`, and less than the export duration; invalid values fail the export. For a partial export, this offset is relative to the exported segment, rather than the page timeline. The engine embeds the exported frame containing that time as a PNG in the MP4 `covr` metadata. Within the exported range, a time between frame boundaries selects the earlier frame, matching playback rather than advancing to the next frame. The cover preserves the exported aspect ratio, fits within 512 × 512 pixels, and is never upscaled. Transparent areas use black, matching video playback. Displaying that cover depends on the player or file browser. ```swift highlight-exportToMp4-cover // Override current playback with a cover from the exported video start. let coverOptions = VideoExportOptions(coverTimeOffset: 0) let coverStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: coverOptions) for try await event in coverStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-cover.mp4")) } } ``` --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To PDF" description: "Export designs as PDF documents with high compatibility mode and underlayer support for special media printing." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [To PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) --- ```swift file=@cesdk_swift_examples/engine-guides-export-to-pdf/ExportToPdf.swift reference-only import Foundation import IMGLYEngine /// Stand-in for a destination that is not a plain file, for example a multipart /// upload. A real implementation sends the bytes on and drops them; collecting /// them would put the whole document back in memory, which is what a streamed /// export exists to avoid. private func upload(_ chunk: Data) throws { print("Sending \(chunk.count) bytes") } @MainActor func exportToPdf(engine: Engine) async throws { // Demo scaffolding: build a small scene with renderable content so every // highlighted snippet has something to export. In your app you would start // from a scene already loaded into the editor instead. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let star = try engine.block.create(.graphic) try engine.block.setShape(star, shape: engine.block.createShape(.star)) try engine.block.setPositionX(star, value: 350) try engine.block.setPositionY(star, value: 250) try engine.block.setWidth(star, value: 100) try engine.block.setHeight(star, value: 100) let starFill = try engine.block.createFill(.color) try engine.block.setColor(starFill, property: "fill/color/value", color: .rgba(r: 0, g: 0, b: 1, a: 1)) try engine.block.setFill(star, fill: starFill) try engine.block.appendChild(to: page, child: star) let exportsDirectory = FileManager.default.temporaryDirectory let pdfBlob = try await engine.block.export(scene, mimeType: .pdf) try pdfBlob.write(to: exportsDirectory.appendingPathComponent("design.pdf")) // Report per-page progress as the PDF is written. The closure runs once per // page; only PDF exports invoke it. let progressBlob = try await engine.block.export( scene, mimeType: .pdf, onProgress: { exportedPages, totalPages in print("Exported \(exportedPages) of \(totalPages) pages") }, ) try progressBlob.write(to: exportsDirectory.appendingPathComponent("design-with-progress.pdf")) // Write the document straight into a file as it is encoded. Nothing buffers // the finished PDF, so peak memory stays bounded by a single page rather than // growing with the page count. try await engine.block.export( scene, to: exportsDirectory.appendingPathComponent("design-streamed.pdf"), mimeType: .pdf, onProgress: { exportedPages, totalPages in print("Streamed \(exportedPages) of \(totalPages) pages") }, ) // Hand the chunks to a destination that is not a plain file. The closure runs // while the encoder does, so a slow destination throttles the encoder instead // of letting chunks queue up. try await engine.block.export(scene) { chunk in try upload(chunk) } // Choose how large a chunk may get. This is the memory held for one chunk, so // lower it for a memory-tight destination and raise it when the per-chunk work // is expensive, for example one request per chunk. try await engine.block.export(scene, options: ExportOptions(pdfChunkSize: 64 * 1024)) { chunk in try upload(chunk) } // Cancelling the task that runs the export stops the export itself. Keep the // task in your view model and cancel it from your Cancel button. let exportTask = Task { try await engine.block.export(scene, mimeType: .pdf) } exportTask.cancel() do { _ = try await exportTask.value } catch { // A cancelled export produces no data. print("Export cancelled: \(error)") } let highCompatibilityOptions = ExportOptions(exportPdfWithHighCompatibility: true) let highCompatibilityBlob = try await engine.block.export( page, mimeType: .pdf, options: highCompatibilityOptions, ) try highCompatibilityBlob.write(to: exportsDirectory.appendingPathComponent("design-high-compatibility.pdf")) engine.editor.setSpotColor(name: "RDG_WHITE", r: 0.8, g: 0.8, b: 0.8) let underlayerOptions = ExportOptions( exportPdfWithHighCompatibility: true, exportPdfWithUnderlayer: true, underlayerSpotColorName: "RDG_WHITE", underlayerOffset: -2.0, ) let underlayerBlob = try await engine.block.export(page, mimeType: .pdf, options: underlayerOptions) try underlayerBlob.write(to: exportsDirectory.appendingPathComponent("design-with-underlayer.pdf")) let a4Options = ExportOptions(targetWidth: 2480, targetHeight: 3508) let a4Blob = try await engine.block.export(page, mimeType: .pdf, options: a4Options) try a4Blob.write(to: exportsDirectory.appendingPathComponent("design-a4.pdf")) } ``` Export your designs as PDF documents with high compatibility mode and underlayer support for special media printing. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-export-to-pdf) PDF provides a universal document format for sharing and printing designs. CE.SDK exports PDF files that preserve vector graphics, support multi-page documents, and include options for print compatibility. You can configure high compatibility mode to ensure consistent rendering across different PDF viewers, and generate underlayers for special media printing like fabric, glass, or DTF transfers. This guide covers exporting designs to PDF, configuring high compatibility mode, controlling the quality of rasterized images, generating underlayers with spot colors, and controlling output dimensions. ## Export to PDF Call `engine.block.export(_:mimeType:options:)` with `MIMEType.pdf` to export a block as a PDF document. The method returns a `Blob` (the engine's `Data` typealias) containing the PDF data, which you can write to disk with `write(to:)`. ```swift highlight-exportToPdf-export let pdfBlob = try await engine.block.export(scene, mimeType: .pdf) try pdfBlob.write(to: exportsDirectory.appendingPathComponent("design.pdf")) ``` Pass the scene ID from `engine.scene.get()` to export every page as a multi-page PDF, or pass a single page ID from `engine.scene.getCurrentPage()` to export just that page. ## Track Export Progress Large multi-page PDFs take time to write. Pass an `onProgress` closure to `export(_:mimeType:options:onProgress:)` to report how far the export has advanced. ```swift highlight-exportToPdf-progress // Report per-page progress as the PDF is written. The closure runs once per // page; only PDF exports invoke it. let progressBlob = try await engine.block.export( scene, mimeType: .pdf, onProgress: { exportedPages, totalPages in print("Exported \(exportedPages) of \(totalPages) pages") }, ) try progressBlob.write(to: exportsDirectory.appendingPathComponent("design-with-progress.pdf")) ``` The closure runs once after each page is serialized into the document, receiving the number of pages exported so far and the total page count. It is PDF-specific: raster exports like PNG or JPEG never invoke it. The closure runs on the main actor, so you can update your UI state directly. ## Stream Large Documents to a File `engine.block.export(_:mimeType:options:)` builds the whole PDF in memory before handing it back as a `Blob`. For large multi-page documents such as photo books and magazines, that buffer becomes the peak allocation and can exhaust memory on everyday devices. The `engine.block.export(_:to:mimeType:options:onProgress:)` overload writes the bytes into a file as they are encoded, so peak memory is bounded by the working set of a single page instead of the size of the finished document. ```swift highlight-exportToPdf-stream // Write the document straight into a file as it is encoded. Nothing buffers // the finished PDF, so peak memory stays bounded by a single page rather than // growing with the page count. try await engine.block.export( scene, to: exportsDirectory.appendingPathComponent("design-streamed.pdf"), mimeType: .pdf, onProgress: { exportedPages, totalPages in print("Streamed \(exportedPages) of \(totalPages) pages") }, ) ``` Streamed export only supports `MIMEType.pdf`; any other mime type throws. The chunks are handed over synchronously while the encoder runs, so a slow destination throttles the encoder rather than letting chunks pile up in memory. The bytes go into a staging file and replace the destination only once the document is complete, so a failed or cancelled export leaves a file that is already at that location untouched and never puts a truncated document in its place. When the destination is not a plain file, for example when you upload the document while it is still being encoded, use the overload that takes a chunk closure: ```swift highlight-exportToPdf-chunks // Hand the chunks to a destination that is not a plain file. The closure runs // while the encoder does, so a slow destination throttles the encoder instead // of letting chunks queue up. try await engine.block.export(scene) { chunk in try upload(chunk) } ``` The closure receives the chunks in order. Throwing from it cancels the export and rethrows your error to the caller. Set `pdfChunkSize` on `ExportOptions` to choose how large a chunk may get. It is the memory you hold for one chunk, so lower it when the destination is memory-tight and raise it when the per-chunk work is expensive, for example one network request per chunk. ```swift highlight-exportToPdf-chunkSize // Choose how large a chunk may get. This is the memory held for one chunk, so // lower it for a memory-tight destination and raise it when the per-chunk work // is expensive, for example one request per chunk. try await engine.block.export(scene, options: ExportOptions(pdfChunkSize: 64 * 1024)) { chunk in try upload(chunk) } ``` The default is an engine-chosen bound of 512 KiB. Any other value is clamped to the range 4 KiB to 64 MiB, so a value outside it makes the trade worse but never fails the export. It is a bound and not a fixed size: the encoder also flushes after each page, so the last chunk of a page is usually smaller. ## Cancel a Running Export `export` follows Swift task cancellation. Cancel the task that runs it, and the export stops. The call throws and returns no data. ```swift highlight-exportToPdf-cancel // Cancelling the task that runs the export stops the export itself. Keep the // task in your view model and cancel it from your Cancel button. let exportTask = Task { try await engine.block.export(scene, mimeType: .pdf) } exportTask.cancel() do { _ = try await exportTask.value } catch { // A cancelled export produces no data. print("Export cancelled: \(error)") } ``` Keep the task in your view model rather than discarding it, because cancelling it is the only way to stop the export. For a multi-page PDF the engine stops at the next page boundary, so the pages that are still queued are never rendered. Any other export stops at once too, for example while it waits for an image to load. ## Configure High Compatibility Mode Set `exportPdfWithHighCompatibility` on `ExportOptions` to rasterize complex elements like gradients with transparency at the scene's DPI. This ensures consistent rendering across PDF viewers. ```swift highlight-exportToPdf-highCompatibility let highCompatibilityOptions = ExportOptions(exportPdfWithHighCompatibility: true) let highCompatibilityBlob = try await engine.block.export( page, mimeType: .pdf, options: highCompatibilityOptions, ) try highCompatibilityBlob.write(to: exportsDirectory.appendingPathComponent("design-high-compatibility.pdf")) ``` Use high compatibility mode when: - Designs contain gradients with transparency - Effects or blend modes render inconsistently across viewers - Maximum compatibility matters more than vector precision High compatibility mode increases file size because complex elements are converted to raster images rather than remaining as vectors. The flag defaults to `true`, so you only need to set it explicitly when you want to disable it. ## Control the Size of Rasterized Images When `exportPdfWithHighCompatibility` is `false`, CE.SDK embeds the original data of unmodified JPEG images directly into the PDF. This keeps exports of photo-heavy documents such as photo books fast and small, because the photos are not decoded and encoded again. CE.SDK must rasterize images that it cannot embed directly, for example images with effects or blurs applied, or every bitmap image when high compatibility mode is enabled. By default these images are encoded losslessly. Set `pdfImageQuality` on `ExportOptions` to a value below `1.0` to encode them as lossy JPEG instead, which produces much smaller files. ```swift let imageQualityOptions = ExportOptions( exportPdfWithHighCompatibility: false, pdfImageQuality: 0.85, ) let pdfBlob = try await engine.block.export(page, mimeType: .pdf, options: imageQualityOptions) ``` Valid values are greater than `0` and at most `1.0`. The default of `1.0` keeps the lossless encoding, in the same way as `webpQuality`. Images that are embedded as their original JPEG data are never encoded again, so this option does not change them. ## Generate Underlayers for Special Media Underlayers provide a base ink layer (typically white) for printing on transparent or non-white substrates like fabric, glass, or acrylic. The underlayer sits behind your design elements and provides opacity on transparent materials. > **Note:** **Warning** Do not flatten the resulting PDF file or you will lose the > underlayer shape, which sits behind your design. ### Define the Underlayer Spot Color Before exporting, define a spot color that represents the underlayer ink. Call `engine.editor.setSpotColor(name:r:g:b:)` to register the color. The RGB values provide a preview representation in PDF viewers; the name must match what your print provider expects. ```swift highlight-exportToPdf-spotColor engine.editor.setSpotColor(name: "RDG_WHITE", r: 0.8, g: 0.8, b: 0.8) ``` Common names include `RDG_WHITE` for Roland DG printers and `White` for other systems. ### Export with Underlayer Options Configure the underlayer spot color name and optional offset. The `underlayerOffset` adjusts the underlayer size in design units — negative values shrink it inward to prevent visible edges from print misalignment (trapping). ```swift highlight-exportToPdf-underlayer let underlayerOptions = ExportOptions( exportPdfWithHighCompatibility: true, exportPdfWithUnderlayer: true, underlayerSpotColorName: "RDG_WHITE", underlayerOffset: -2.0, ) let underlayerBlob = try await engine.block.export(page, mimeType: .pdf, options: underlayerOptions) try underlayerBlob.write(to: exportsDirectory.appendingPathComponent("design-with-underlayer.pdf")) ``` The underlayer is generated automatically from the contours of all design elements on the page. Elements with transparency will have proportionally reduced underlayer opacity. ## Add Printer's Marks A print shop needs to see where to cut, and a press operator needs a target to align the plates against. Enable `exportPdfWithCropMarks` for the four pairs of corner lines that mark the cut, and `exportPdfWithRegistrationMarks` for a bullseye at the middle of each page edge. Both default to `false`, so an export that does not ask for marks is unchanged. ```swift let printMarkOptions = ExportOptions( exportPdfWithCropMarks: true, exportPdfWithRegistrationMarks: true, printMarkOffset: 3.0, cropMarkLength: 5.0, printMarkWidth: 0.25, ) let pdfBlob = try await engine.block.export(page, mimeType: .pdf, options: printMarkOptions) ``` Both mark types are painted in the PDF registration colorant `All`, so they appear on every separation, and each is knocked out in white underneath so it stays legible over dark artwork. `printMarkOffset` and `cropMarkLength` are in design units, so the offset is directly comparable with the page bleed and the sheet grows by the sum of the two. `printMarkWidth` is in points, the unit a print shop states a stroke weight in. A negative offset falls back to the 6 pt default, and `0` places a mark on the trim edge. The offset is measured from the trim, so a small offset places a mark inside the bleed, which the knife removes anyway. Marks grow the exported page by the offset plus the equivalent of 15 points on each side, or by the offset plus `cropMarkLength` when that is longer. The artwork does not move, and the TrimBox and BleedBox keep describing the same physical rectangles. A page whose trim is not a rectangle gets no marks and no larger page. ## Export at Target Dimensions Use `targetWidth` and `targetHeight` on `ExportOptions` to control the exported PDF dimensions in pixels. The block renders large enough to fill the target size while maintaining aspect ratio. ```swift highlight-exportToPdf-targetSize let a4Options = ExportOptions(targetWidth: 2480, targetHeight: 3508) let a4Blob = try await engine.block.export(page, mimeType: .pdf, options: a4Options) try a4Blob.write(to: exportsDirectory.appendingPathComponent("design-a4.pdf")) ``` For print output, calculate the target dimensions from your desired DPI: - A4 at 300 DPI: 2480 × 3508 pixels - Letter at 300 DPI: 2550 × 3300 pixels ## PDF Export Options `mimeType` is the second argument to `engine.block.export(_:mimeType:options:)`. The remaining fields below are properties on `ExportOptions`. | Option | Description | | ------ | ----------- | | `mimeType` | Output format. Pass `MIMEType.pdf`. | | `exportPdfWithHighCompatibility` | Rasterize complex elements at scene DPI for consistent rendering. Defaults to `true`. | | `pdfImageQuality` | Encoding quality for images that CE.SDK has to rasterize. Values below the default of `1.0` encode them as lossy JPEG. | | `exportPdfWithUnderlayer` | Generate an underlayer from design contours. Defaults to `false`. | | `underlayerSpotColorName` | Spot color name for the underlayer ink. Required when `exportPdfWithUnderlayer` is `true`. | | `underlayerOffset` | Size adjustment in design units. Negative values shrink the underlayer inward. | | `exportPdfWithCropMarks` | Draw the four pairs of corner lines that show a print shop where to cut. Defaults to `false`. | | `exportPdfWithRegistrationMarks` | Draw a bullseye target at the middle of each page edge, which a press operator aligns the plates by. Defaults to `false`. | | `printMarkOffset` | Distance in design units from the trim to the nearest edge of any mark, so it is comparable with the page bleed. Shared by both mark types. Defaults to 6 pt. | | `printMarkWidth` | Stroke weight in points of any mark, the unit a print shop states a weight in. Shared by both mark types. Defaults to `0.25`. | | `cropMarkLength` | Length in design units of one crop mark line, the same unit as `printMarkOffset`. Defaults to 15 pt. | | `targetWidth` | Target output width in pixels. Must be used with `targetHeight`. | | `targetHeight` | Target output height in pixels. Must be used with `targetWidth`. | | `pdfChunkSize` | Upper bound in bytes for a single chunk of a streamed PDF export. Defaults to `0`, which uses the engine bound of 512 KiB. Other values are clamped to 4 KiB to 64 MiB. | ## API Reference | Method | Description | | ------ | ----------- | | `engine.block.export(_:mimeType:options:onProgress:)` | Export a block as PDF with format and compatibility options, optionally reporting per-page progress through the `onProgress` closure | | `engine.block.export(_:to:mimeType:options:onProgress:)` | Export a block as PDF straight into a file, without holding the document in memory | | `engine.block.export(_:mimeType:options:onProgress:onData:)` | Export a block as PDF, delivering the bytes chunk by chunk to a closure | | `engine.editor.setSpotColor(name:r:g:b:)` | Define a spot color for underlayer ink | | `engine.scene.get()` | Get the scene for multi-page PDF export | | `engine.scene.getCurrentPage()` | Get the current page for single-page export | ## Next Steps - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Compare all supported export formats - [Export for Printing](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-printing-bca896/) — Print workflows with DPI and color management - [Spot Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/spot-c3a150/) — Define and use spot colors in designs - [Export Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/) — Check device limits before exporting large designs --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To PNG" description: "Export CE.SDK designs to PNG format with lossless compression and full alpha support for graphics, UI elements, and content with transparency." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-png-f87eaf/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [To PNG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-png-f87eaf/) > [Conversion](https://img.ly/docs/cesdk/mac-catalyst/conversion-c3fbb3/) > [To PNG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-png-f87eaf/) --- Export CE.SDK designs to PNG format with full alpha support and lossless compression—ideal for graphics, UI elements, and any content where transparency or pixel-perfect edges matter. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-export-to-png) PNG uses lossless compression and preserves transparency through an alpha channel. The encoder lets you trade encoding speed for file size without affecting image quality. ```swift file=@cesdk_swift_examples/engine-guides-export-to-png/ToPng.swift reference-only import Foundation import IMGLYEngine @MainActor func toPng(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL try engine.editor.setSettingString("basePath", value: baseURL.absoluteString) let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: sceneURL) let page = try engine.scene.getPages().first! let blob: Blob = try await engine.block.export(page, mimeType: .png) let compressedBlob = try await engine.block.export( page, mimeType: .png, options: ExportOptions(pngCompressionLevel: 9), ) let sizedBlob = try await engine.block.export( page, mimeType: .png, options: ExportOptions(targetWidth: 1920, targetHeight: 1080), ) let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("export.png") try blob.write(to: outputURL) _ = compressedBlob _ = sizedBlob } ``` This guide covers exporting to PNG, configuring compression and dimensions, and saving exports to disk. ## Export to PNG Export a design block by calling `engine.block.export(_:mimeType:options:)` with `.png` as the MIME type. The call returns a `Blob` (a `Data` value) containing the encoded image. ```swift highlight-toPng-exportPng let blob: Blob = try await engine.block.export(page, mimeType: .png) ``` Pass a page returned by `engine.scene.getPages()`, or any other block ID, to export specific elements. ## Export Options PNG export reads these fields from `ExportOptions`: | Option | Type | Default | Description | | --------------------- | ------- | ------- | ------------------------------------------------------------------------------------ | | `pngCompressionLevel` | `Int` | `5` | Compression level from `0` (fastest) to `9` (smallest). Quality is unaffected | | `targetWidth` | `Float` | `0` | Output width in pixels. Used together with `targetHeight`; `0` disables the override | | `targetHeight` | `Float` | `0` | Output height in pixels. Used together with `targetWidth`; `0` disables the override | | `allowTextOverhang` | `Bool` | `false` | When `true`, text blocks export with the full glyph bounds visible | ### Compression Level The `pngCompressionLevel` field (`0`–`9`) controls the trade-off between file size and encoding speed. Higher values produce smaller files but take longer to encode. PNG compression is lossless, so quality is never affected. ```swift highlight-toPng-compressionLevel let compressedBlob = try await engine.block.export( page, mimeType: .png, options: ExportOptions(pngCompressionLevel: 9), ) ``` - `0` — No compression, fastest encoding - `5` — Balanced (default) - `9` — Maximum compression, slowest encoding ### Target Dimensions Specify `targetWidth` and `targetHeight` together to export at exact dimensions. The output fills the target size while maintaining aspect ratio. ```swift highlight-toPng-targetSize let sizedBlob = try await engine.block.export( page, mimeType: .png, options: ExportOptions(targetWidth: 1920, targetHeight: 1080), ) ``` If the target aspect ratio differs from the block's aspect ratio, the output extends beyond the target on one axis to preserve proportions. ## Save to File System The returned `Blob` is a `Data` value, so writing it to disk is a single call to `write(to:)`. ```swift highlight-toPng-saveFile let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("export.png") try blob.write(to: outputURL) ``` ## When to Use PNG PNG works well for: - Graphics with sharp edges, text, and UI elements - Designs that require transparency - Logos, icons, and illustrations where pixel-perfect output matters > **Note:** For photographs and images with smooth color gradients, JPEG or WebP usually produces smaller files. See [Export to JPEG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-jpeg-6f88e9/) for the photo-friendly alternative. ## Troubleshooting **File too large** — Increase `pngCompressionLevel` toward `9`, or reduce dimensions with `targetWidth` and `targetHeight`. For photographic content, switch to JPEG or WebP. **Encoding feels slow** — Lower `pngCompressionLevel` toward `0`. The default `5` is balanced; `0` disables compression entirely for the fastest encode. **Transparent areas appear black** — Ensure the page or block has a transparent background fill. PNG preserves alpha when the source block is transparent. ## API Reference | Method | Description | | ------------------------------------------ | ------------------------------------------------------------------------------------------ | | `engine.block.export(_:mimeType:options:)` | Export a block to the specified format | | `engine.scene.load(from:)` | Load a scene from a URL (local or remote) | | `engine.scene.getPages()` | Return all pages in the current scene | | `ExportOptions` | Format-specific export configuration; PNG reads `pngCompressionLevel`, `targetWidth`, `targetHeight`, `allowTextOverhang` | ## Next Steps - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Compare all available export formats - [Export to JPEG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-jpeg-6f88e9/) — Use the photo-friendly format when transparency isn't needed - [Partial Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/) — Export specific blocks, groups, or page elements instead of entire scenes - [Export with a Color Mask](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/with-color-mask-4f868f/) — Remove specific colors and generate alpha masks during PNG export --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To Raw Data" description: "Export CE.SDK designs to uncompressed RGBA pixel data for custom image processing, Core Graphics rendering, and integration with advanced imaging pipelines." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-raw-data-abd7da/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [To Raw Data](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-raw-data-abd7da/) --- Export CE.SDK designs to raw RGBA pixel data—ideal when you need direct pixel access for custom processing, Core Graphics rendering, or integration with imaging pipelines that bypass standard image encoders. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-export-to-raw-data) ```swift file=@cesdk_swift_examples/engine-guides-export-to-raw-data/ToRawData.swift reference-only import CoreGraphics import Foundation import IMGLYEngine @MainActor func toRawData(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL try engine.editor.setSettingString("basePath", value: baseURL.absoluteString) let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: sceneURL) let page = try engine.scene.getPages().first! let exportsDirectory = FileManager.default.temporaryDirectory let width = 1920 let height = 1080 let pixelData: Blob = try await engine.block.export( page, mimeType: .binary, options: ExportOptions(targetWidth: Float(width), targetHeight: Float(height)), ) try pixelData.write(to: exportsDirectory.appendingPathComponent("design.rgba")) let centerX = width / 2 let centerY = height / 2 let centerIndex = (centerY * width + centerX) * 4 let red = pixelData[centerIndex] let green = pixelData[centerIndex + 1] let blue = pixelData[centerIndex + 2] let alpha = pixelData[centerIndex + 3] print("Center pixel RGBA: \(red), \(green), \(blue), \(alpha)") let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) let provider = CGDataProvider(data: pixelData as NSData)! let cgImage = CGImage( width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent, )! let resizedOptions = ExportOptions(targetWidth: 960, targetHeight: 540) let resizedPixelData = try await engine.block.export(page, mimeType: .binary, options: resizedOptions) try resizedPixelData.write(to: exportsDirectory.appendingPathComponent("design.thumbnail.rgba")) let maxExportSize = try engine.editor.getMaxExportSize() print("Maximum export dimension: \(maxExportSize)px") _ = cgImage } ``` This guide covers exporting to raw RGBA bytes, reading individual pixels, converting the data to a `CGImage`, and controlling the output resolution. ## When to Use Raw Data Export Raw pixel data export gives you direct access to uncompressed RGBA bytes, with complete control over individual pixels for custom processing. Reach for raw data when you need pixel-level access for custom algorithms or integrations. For standard image delivery, use PNG or JPEG instead—they apply compression and are ready to display or persist without additional work. ## Understanding Raw Data Format When you export with `MIMEType.binary`, CE.SDK returns a `Blob` (a typealias for `Data`) containing uncompressed RGBA pixel data: - **4 bytes per pixel** representing Red, Green, Blue, and Alpha channels - **Values from 0–255** for each channel (8-bit unsigned integers) - **Row-major order** with pixels arranged left-to-right, top-to-bottom - **Total size** equals width × height × 4 bytes ## How to Export Raw Data Export a block as raw pixel data by calling `engine.block.export(_:mimeType:options:)` with `.binary` as the MIME type. To make the returned buffer's dimensions deterministic, pair the call with `targetWidth` and `targetHeight` on `ExportOptions`—those values define the output pixel size exactly, so the `width × height × 4` formula always describes the buffer. The example below targets 1920×1080. Pick whatever pixel size your downstream pipeline expects. ```swift highlight-toRawData-export let width = 1920 let height = 1080 let pixelData: Blob = try await engine.block.export( page, mimeType: .binary, options: ExportOptions(targetWidth: Float(width), targetHeight: Float(height)), ) try pixelData.write(to: exportsDirectory.appendingPathComponent("design.rgba")) ``` The returned `Blob` is a `Data` value containing the RGBA buffer; the rest of this guide reads, transforms, and re-encodes it. ## Download Exported Data Once you have the raw bytes, you can inspect them directly, hand them to Core Graphics, or re-encode them to a standard image format. Because `Blob` is just `Data`, all of Swift's existing data-handling APIs work without an intermediate decode step. ### Read Individual Pixels Index into the buffer at `(y * width + x) * 4`. The four bytes at that offset are Red, Green, Blue, and Alpha respectively—useful for color sampling, brightness analysis, custom filters, or feeding pixels into a machine-learning pipeline. ```swift highlight-toRawData-readPixels let centerX = width / 2 let centerY = height / 2 let centerIndex = (centerY * width + centerX) * 4 let red = pixelData[centerIndex] let green = pixelData[centerIndex + 1] let blue = pixelData[centerIndex + 2] let alpha = pixelData[centerIndex + 3] print("Center pixel RGBA: \(red), \(green), \(blue), \(alpha)") ``` ### Convert to a CGImage To display the raw bytes or hand them to other Apple imaging APIs, wrap the buffer in a `CGDataProvider` and construct a `CGImage` directly. No PNG decode step is needed because the data is already pre-decoded RGBA. The buffer's alpha is **premultiplied**—each color channel is already scaled by the alpha value—so pass `CGImageAlphaInfo.premultipliedLast` when constructing the `CGImage`. Using `.last` (straight alpha) would composite translucent pixels too dark. ```swift highlight-toRawData-toCGImage let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) let provider = CGDataProvider(data: pixelData as NSData)! let cgImage = CGImage( width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent, )! ``` Pass `cgImage` to an image type to display the result, or to `CGImageDestination` to re-encode it as PNG, JPEG, or any other format supported by ImageIO. ## Performance Considerations Raw RGBA data grows linearly with pixel count: a 1920×1080 export consumes about 8.3 MB, compared to 1–3 MB for the same scene exported as PNG. Two settings on the engine help keep memory usage bounded. ### Reduce the Export Resolution Pass `targetWidth` and `targetHeight` on `ExportOptions` to render at a smaller resolution while preserving the block's aspect ratio. Both fields default to `0`, which disables the override—setting either field activates the target-size path. ```swift highlight-toRawData-targetSize let resizedOptions = ExportOptions(targetWidth: 960, targetHeight: 540) let resizedPixelData = try await engine.block.export(page, mimeType: .binary, options: resizedOptions) try resizedPixelData.write(to: exportsDirectory.appendingPathComponent("design.thumbnail.rgba")) ``` ### Check Export Size Limits Before exporting very large blocks, query the engine for the maximum supported dimension. `getMaxExportSize()` returns the side length cap (in pixels) for either width or height. Use it to clamp `targetWidth`/`targetHeight` ahead of time and avoid failures during export. ```swift highlight-toRawData-checkLimits let maxExportSize = try engine.editor.getMaxExportSize() print("Maximum export dimension: \(maxExportSize)px") ``` ### When to Use Raw vs. Compressed - **Use raw data** when you need custom post-processing on CE.SDK exports before delivery - **Use raw data** for intermediate steps in multi-stage pipelines, or for GPU uploads that would otherwise pay a PNG decode cost - **Use PNG or JPEG** when the output is going straight to disk, the user, or a network—compressed formats are smaller and ready to display - **Reach for `CGImage` and ImageIO** when you want a familiar Apple API surface; reach for raw RGBA when your code needs the bytes themselves ## API Reference | API | Description | | --- | --- | | `engine.block.export(_:mimeType:options:)` | Export a block; pass `.binary` for raw RGBA data | | `MIMEType.binary` | The `application/octet-stream` MIME type that selects the raw RGBA pipeline | | `ExportOptions` | Configures `targetWidth` and `targetHeight` to control the output resolution | | `engine.editor.getMaxExportSize()` | Returns the maximum supported export side length in pixels | ## Next Steps - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Compare all available export formats - [Export to PNG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-png-f87eaf/) — Compressed image export with transparency - [Export to JPEG](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-jpeg-6f88e9/) — Lossy compression for photographs - [Export to PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) — Vector export for print and document workflows - [Partial Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/) — Export specific blocks, groups, or pages instead of the whole scene --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To WebM" description: "Export video compositions as WebM files with the royalty-free VP8, VP9 or AV1 video codecs and Opus or Vorbis audio, with optional transparency." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-webm-764271/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [To WebM](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-webm-764271/) --- Export video compositions as WebM files with VP8, VP9 or AV1 video and Opus or Vorbis audio, and keep the transparency of the page. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-export-to-webm) WebM is an open video format. Its codecs are royalty-free, so you can encode and publish them without a patent license. The engine bundles the VP8, VP9, AV1, Opus and Vorbis encoders, so a WebM export works the same on iOS and macOS, with no system codec. ```swift file=@cesdk_swift_examples/engine-guides-export-to-webm/ExportToWebm.swift reference-only import Foundation import IMGLYEngine @MainActor func exportToWebm(engine: Engine) async throws { // Demo scaffolding: build a video scene with a single page and a video fill so // the exportVideo calls below have something to encode. In your app you would // start from a scene already loaded into the editor instead. let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 2) let baseURL = try engine.guidesBaseURL let video = try engine.block.create(.graphic) try engine.block.setShape(video, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) try engine.block.setURL( videoFill, property: "fill/video/fileURI", value: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) try engine.block.setFill(video, fill: videoFill) try engine.block.appendChild(to: page, child: video) try engine.block.fillParent(video) let exportsDirectory = FileManager.default.temporaryDirectory let videoStream = try await engine.block.exportVideo(page, mimeType: .webm) for try await event in videoStream { switch event { case let .progress(_, encoded, total): print("Encoded \(encoded) of \(total) frames") case let .finished(video: blob): try blob.write(to: exportsDirectory.appendingPathComponent("video.webm")) } } let codecOptions = VideoExportOptions( videoBitrate: 4_000_000, videoCodec: .vp8, audioCodec: .vorbis, ) let codecStream = try await engine.block.exportVideo(page, mimeType: .webm, options: codecOptions) for try await event in codecStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-vp8.webm")) } } // Shrink the video and hide the page background, so the area around the video is transparent. try engine.block.setFillEnabled(page, enabled: false) try engine.block.setWidth(video, value: 640) try engine.block.setHeight(video, value: 360) let alphaOptions = VideoExportOptions(videoCodec: .vp9, exportAlpha: true) let alphaStream = try await engine.block.exportVideo(page, mimeType: .webm, options: alphaOptions) for try await event in alphaStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-alpha.webm")) } } } ``` This guide covers how to export a page to WebM, choose the codecs and keep the transparency of the page. ## Export to WebM Call `engine.block.exportVideo(_:mimeType:options:)` with a page block and `MIMEType.webm`. The call returns an `AsyncThrowingStream` that yields `.progress` events while it encodes, and a final `.finished(video:)` event with the encoded `Blob`. ```swift highlight-exportToWebm-exportVideo let videoStream = try await engine.block.exportVideo(page, mimeType: .webm) for try await event in videoStream { switch event { case let .progress(_, encoded, total): print("Encoded \(encoded) of \(total) frames") case let .finished(video: blob): try blob.write(to: exportsDirectory.appendingPathComponent("video.webm")) } } ``` Without options, the export encodes VP9 video and Opus audio. Every other `VideoExportOptions` value, such as `targetWidth`, `framerate`, `videoBitrate`, `timeOffset` and `duration`, works the same as for an [MP4 export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-mp4-c998a8/). So does cancellation. ## Choose the Codecs Set `videoCodec` and `audioCodec` in `VideoExportOptions` to choose the codecs. A WebM file holds `.vp8`, `.vp9` or `.av1` video and `.opus` or `.vorbis` audio. ```swift highlight-exportToWebm-codecs let codecOptions = VideoExportOptions( videoBitrate: 4_000_000, videoCodec: .vp8, audioCodec: .vorbis, ) let codecStream = try await engine.block.exportVideo(page, mimeType: .webm, options: codecOptions) for try await event in codecStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-vp8.webm")) } } ``` - VP8 is the oldest of the three video codecs, and almost every player decodes it. - AV1 makes the smallest file at the same quality, but it takes the longest to encode. - A codec that WebM cannot hold, such as `.h264` or `.aac`, fails the export. The engine does not change the codec for you. ## Keep the Transparency Set `exportAlpha: true` to keep the transparent areas of the page. The WebM file then has a second video stream with the transparency. Only `.vp8` and `.vp9` keep it. ```swift highlight-exportToWebm-alpha // Shrink the video and hide the page background, so the area around the video is transparent. try engine.block.setFillEnabled(page, enabled: false) try engine.block.setWidth(video, value: 640) try engine.block.setHeight(video, value: 360) let alphaOptions = VideoExportOptions(videoCodec: .vp9, exportAlpha: true) let alphaStream = try await engine.block.exportVideo(page, mimeType: .webm, options: alphaOptions) for try await event in alphaStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-alpha.webm")) } } ``` - Hide the page fill, and leave part of the page uncovered. Otherwise no area of the page is transparent. - `exportAlpha` with `.av1` or with `MIMEType.mp4` fails the export. - Whether a player shows the transparency depends on the player. AVFoundation does not play WebM. ## Troubleshooting - **The export fails with `ENCODE.VIDEO_CODEC_CONTAINER_MISMATCH` or `ENCODE.AUDIO_CODEC_CONTAINER_MISMATCH`**: The codec does not fit the file type. Use `.vp8`, `.vp9` or `.av1` with `.opus` or `.vorbis` for WebM, or leave the codecs at `.auto`. - **The export fails with `ENCODE.VIDEO_ALPHA_UNSUPPORTED`**: Use `.vp8` or `.vp9` with `MIMEType.webm` when `exportAlpha` is `true`. - **The export is slow**: The engine encodes WebM in software. Use VP9 or VP8 instead of AV1, or lower `targetWidth`, `targetHeight` and `framerate`. ## API Reference | Method | Description | | --- | --- | | `engine.block.exportVideo(_:mimeType:options:)` | Export a page block as a video. Returns an `AsyncThrowingStream`. | | `VideoExportOptions(videoBitrate:videoCodec:audioCodec:exportAlpha:)` | Select the codecs, the transparency stream and the bitrate. | | `engine.block.setFillEnabled(_:enabled:)` | Hide the page fill before a transparent export. | ## WebM Export Options | Option | Type | Default | Description | | --- | --- | --- | --- | | `videoCodec` | `VideoCodec` | `.auto` | `.vp8`, `.vp9` or `.av1`. `.auto` picks VP9 for `.webm`. | | `audioCodec` | `AudioCodec` | `.auto` | `.opus` or `.vorbis`. `.auto` picks Opus for `.webm`. | | `exportAlpha` | `Bool` | `false` | Keeps the transparency of the page. Only `.vp8` and `.vp9`. | ## Next Steps - [Export to MP4](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-mp4-c998a8/) - Export with H.264 or HEVC, track progress and cancel an export - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) - Compare all supported export formats - [Partial Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/) - Export specific blocks or timeline segments --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "To WebP" description: "Export CE.SDK designs to WebP format with lossy and lossless compression for smaller files than PNG or JPEG at comparable quality." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-webp-aef6f4/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [To WebP](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-webp-aef6f4/) --- Export CE.SDK designs to WebP format for compact image files that preserve transparency, with a single quality knob spanning lossy and lossless modes. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-export-to-webp) WebP delivers smaller files than PNG while preserving transparency, and smaller files than JPEG at comparable quality. Use it when bandwidth or storage matters and the output stays inside a WebP-aware viewer. ```swift file=@cesdk_swift_examples/engine-guides-export-to-webp/ToWebp.swift reference-only import Foundation import IMGLYEngine @MainActor func toWebp(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL try engine.editor.setSettingString("basePath", value: baseURL.absoluteString) let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: sceneURL) let page = try engine.scene.getPages().first! let blob: Blob = try await engine.block.export( page, mimeType: .webp, options: ExportOptions(webpQuality: 0.8), ) let losslessBlob = try await engine.block.export( page, mimeType: .webp, options: ExportOptions(webpQuality: 1.0), ) let sizedBlob = try await engine.block.export( page, mimeType: .webp, options: ExportOptions( webpQuality: 0.85, targetWidth: 1920, targetHeight: 1080, ), ) let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("export.webp") try blob.write(to: outputURL) _ = losslessBlob _ = sizedBlob } ``` This guide covers exporting to WebP, configuring quality, resizing the output, and saving the result to disk. ## Export to WebP Export a design block by calling `engine.block.export(_:mimeType:options:)` with `.webp` as the MIME type. The call returns a `Blob` (a `Data` value) containing the encoded image. The `webpQuality` field on `ExportOptions` controls compression — `0.8` is a good starting point for web delivery. ```swift highlight-toWebp-exportWebp let blob: Blob = try await engine.block.export( page, mimeType: .webp, options: ExportOptions(webpQuality: 0.8), ) ``` Pass a page returned by `engine.scene.getPages()`, or any other block ID, to export specific elements. ## Export Options WebP export reads these fields from `ExportOptions`: | Option | Type | Default | Description | | -------------- | ------- | ------- | ------------------------------------------------------------------------------------ | | `webpQuality` | `Float` | `1.0` | Quality from just above `0` to `1.0`. `1.0` switches the encoder to lossless mode | | `targetWidth` | `Float` | `0` | Output width in pixels. Used together with `targetHeight`; `0` disables the override | | `targetHeight` | `Float` | `0` | Output height in pixels. Used together with `targetWidth`; `0` disables the override | ### Lossless Compression Set `webpQuality` to `1.0` to switch the encoder to its lossless mode. WebP's lossless encoding usually produces smaller files than PNG while preserving every pixel. ```swift highlight-toWebp-lossless let losslessBlob = try await engine.block.export( page, mimeType: .webp, options: ExportOptions(webpQuality: 1.0), ) ``` Values between `0.8` and `0.95` keep visual quality high while shrinking the file substantially. Lower values trade more visible quality for smaller files. ### Target Dimensions Specify `targetWidth` and `targetHeight` together to export at exact dimensions. The output fills the target size while maintaining aspect ratio. ```swift highlight-toWebp-targetSize let sizedBlob = try await engine.block.export( page, mimeType: .webp, options: ExportOptions( webpQuality: 0.85, targetWidth: 1920, targetHeight: 1080, ), ) ``` If the target aspect ratio differs from the block's aspect ratio, the output extends beyond the target on one axis to preserve proportions. ## Save to File System The returned `Blob` is a `Data` value, so writing it to disk is a single call to `write(to:)`. ```swift highlight-toWebp-saveFile let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("export.webp") try blob.write(to: outputURL) ``` ## Troubleshooting **File still too large** — Lower `webpQuality` toward `0.5`–`0.7`, or reduce dimensions with `targetWidth` and `targetHeight`. Pure-photographic content compresses well at lower quality settings. **Transparent areas appear opaque** — Verify the source page or block has a transparent background fill. WebP preserves alpha when the source block is transparent. **Output looks degraded at high quality** — Switch to lossless by setting `webpQuality` to `1.0`. For graphics with sharp edges and text, lossless WebP avoids the smearing that lossy compression can introduce. ## API Reference | Method | Description | | ------------------------------------------ | -------------------------------------------------------------------------- | | `engine.block.export(_:mimeType:options:)` | Export a block to the specified format | | `engine.scene.load(from:)` | Load a scene from a URL (local or remote) | | `engine.scene.getPages()` | Return all pages in the current scene | | `ExportOptions` | Format-specific export configuration; WebP reads `webpQuality`, `targetWidth`, `targetHeight` | ## Next Steps - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Compare all available export formats - [Export to PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) — Generate print-ready PDF documents from your designs - [Partial Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/) — Export specific blocks, groups, or page elements instead of entire scenes - [Export Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/) — Check device export limits before exporting large designs --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Export with a Color Mask" description: "Export design blocks with color masking in CE.SDK to remove specific colors and generate alpha masks for print workflows and compositing." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/with-color-mask-4f868f/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [With a Color Mask](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/with-color-mask-4f868f/) --- Remove specific colors from exported images and generate alpha masks using CE.SDK's color mask export API for print workflows, transparency creation, and compositing pipelines. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-export-with-color-mask) When exporting, CE.SDK can remove specific RGB colors by replacing matching pixels with transparency. The export generates two files: the masked image with transparent areas and an alpha mask showing removed pixels. ```swift file=@cesdk_swift_examples/engine-guides-export-with-color-mask/ExportWithColorMask.swift reference-only import Foundation import IMGLYEngine @MainActor func exportWithColorMask(engine: Engine) async throws { // Demo scaffolding: build a small scene with two graphic blocks so the // exported PNG visibly demonstrates color masking — a pure-red rectangle // (which the mask removes) and a blue ellipse (which survives). // In your app you would start from a scene already loaded into the editor. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let registrationMark = try engine.block.create(.graphic) try engine.block.setShape(registrationMark, shape: engine.block.createShape(.rect)) let redFill = try engine.block.createFill(.color) try engine.block.setColor(redFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.0, b: 0.0, a: 1.0)) try engine.block.setFill(registrationMark, fill: redFill) try engine.block.setPositionX(registrationMark, value: 50) try engine.block.setPositionY(registrationMark, value: 50) try engine.block.setWidth(registrationMark, value: 200) try engine.block.setHeight(registrationMark, value: 200) try engine.block.appendChild(to: page, child: registrationMark) let artwork = try engine.block.create(.graphic) try engine.block.setShape(artwork, shape: engine.block.createShape(.ellipse)) let blueFill = try engine.block.createFill(.color) try engine.block.setColor(blueFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0)) try engine.block.setFill(artwork, fill: blueFill) try engine.block.setPositionX(artwork, value: 300) try engine.block.setPositionY(artwork, value: 100) try engine.block.setWidth(artwork, value: 400) try engine.block.setHeight(artwork, value: 400) try engine.block.appendChild(to: page, child: artwork) let blobs = try await engine.block.exportWithColorMask( page, mimeType: .png, maskColorR: 1.0, maskColorG: 0.0, maskColorB: 0.0, ) let maskedImage = blobs[0] let alphaMask = blobs[1] let exportsDirectory = FileManager.default.temporaryDirectory try maskedImage.write(to: exportsDirectory.appendingPathComponent("design.masked.png")) try alphaMask.write(to: exportsDirectory.appendingPathComponent("design.alpha.png")) } ``` Color mask exports work through exact RGB color matching — pixels that precisely match your specified color values (0.0–1.0 range) are removed. This is useful for print workflows (removing registration marks), transparency creation (removing background colors), or generating alpha masks for compositing tools. ## Exporting with Color Masks Export blocks with color masking using the `exportWithColorMask` method. This method removes pixels matching the specified RGB color from the rendered output and returns both a masked image and an alpha mask. ```swift highlight-exportWithColorMask-export let blobs = try await engine.block.exportWithColorMask( page, mimeType: .png, maskColorR: 1.0, maskColorG: 0.0, maskColorB: 0.0, ) let maskedImage = blobs[0] let alphaMask = blobs[1] ``` The method accepts the block to export, a `MIMEType`, three RGB color components as `Float` values in the 0.0–1.0 range, and optional `ExportOptions`. This example uses pure red `(1.0, 0.0, 0.0)` to identify and remove registration marks from the design. The call returns an array of two `Blob` values (a `Blob` is `Foundation.Data`). The first element is the masked image with transparency applied where the specified color was found. The second element is the alpha mask — a black-and-white image showing which pixels were removed (black) and which remained (white). > **Note:** Color matching is exact and bytewise. Anti-aliased edges between the mask color and another color are not removed, gradient stops that pass near the mask color render normally, and lossy formats like JPEG can shift pixels by a single bit and skip the match. Reserve mask colors for solid fills you control. ### Specifying RGB Color Values RGB color components in CE.SDK use floating-point values from 0.0 to 1.0, not the 0–255 integer values common in design tools: - Pure red: `(1.0, 0.0, 0.0)` — Common for registration marks - Pure magenta: `(1.0, 0.0, 1.0)` — Distinctive marker color - Pure cyan: `(0.0, 1.0, 1.0)` — Alternative marker color - Pure yellow: `(1.0, 1.0, 0.0)` — Useful for exclusion zones When converting from standard 0–255 RGB values, divide each component by 255. For example, RGB(255, 128, 0) becomes `(1.0, 0.502, 0.0)`. ## How to Export with Color Masks A `Blob` is a `Foundation.Data` instance, so you can persist both outputs with the standard `write(to:)` API. This snippet writes the masked image and alpha mask side-by-side into the temporary directory so you can pick them up from your file pipeline or upload them to a print service. ```swift highlight-exportWithColorMask-write let exportsDirectory = FileManager.default.temporaryDirectory try maskedImage.write(to: exportsDirectory.appendingPathComponent("design.masked.png")) try alphaMask.write(to: exportsDirectory.appendingPathComponent("design.alpha.png")) ``` The masked image is print-ready with the specified color removed. The alpha mask shows exactly where pixels were removed, useful for verification or compositing in external applications. ## API Reference | Method | Description | | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `engine.block.exportWithColorMask(_:mimeType:maskColorR:maskColorG:maskColorB:options:)` | Exports a block with the specified RGB color removed, returning `[maskedImage, alphaMask]`. | | `engine.block.export(_:mimeType:options:)` | Exports a block without color masking. | | `engine.block.createFill(_:)` | Creates a fill definition that you can attach to a block. | | `engine.block.setColor(_:property:color:)` | Sets a color value on a fill property. | ## Next Steps - [Export Options](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Explore every supported export format and the options each one accepts. - [Export to PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) — Produce print-ready PDFs with optional underlayers for spot-color workflows. - [Partial Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/) — Export individual blocks or groups instead of the full page. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Export for Printing" description: "Export designs from CE.SDK as print-ready PDFs with professional output options including high compatibility mode, underlayers for special media, and scene DPI configuration." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-printing-bca896/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [For Printing](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-printing-bca896/) --- Export print-ready PDFs from CE.SDK with options for high compatibility mode, underlayers for special media like fabric or glass, and configurable output resolution. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-export-for-printing) CE.SDK exports designs as PDFs, but professional print workflows require specific configurations beyond standard export. This guide covers PDF export options for print, including high compatibility mode for complex designs, underlayers for printing on special media, and output resolution settings. ```swift file=@cesdk_swift_examples/engine-guides-export-for-printing/ExportForPrinting.swift reference-only import Foundation import IMGLYEngine @MainActor func exportForPrinting(engine: Engine) async throws { // Demo scaffolding: build a small scene with one renderable graphic so the // PDF exports below produce a non-empty page. In your app you would start // from a scene that the editor has already loaded. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let star = try engine.block.create(.graphic) try engine.block.setShape(star, shape: engine.block.createShape(.star)) try engine.block.setPositionX(star, value: 250) try engine.block.setPositionY(star, value: 150) try engine.block.setWidth(star, value: 300) try engine.block.setHeight(star, value: 300) let starFill = try engine.block.createFill(.color) try engine.block.setColor(starFill, property: "fill/color/value", color: .rgba(r: 0, g: 0, b: 1, a: 1)) try engine.block.setFill(star, fill: starFill) let exportsDirectory = FileManager.default.temporaryDirectory // 300 DPI is standard for high-quality print output. try engine.block.setFloat(scene, property: "scene/dpi", value: 300) // High compatibility mode rasterizes complex elements like gradients with // transparency at the scene's DPI so they render consistently across PDF // viewers and print RIPs. let highCompatibilityOptions = ExportOptions(exportPdfWithHighCompatibility: true) let highCompatibilityPdf = try await engine.block.export( page, mimeType: .pdf, options: highCompatibilityOptions, ) try highCompatibilityPdf.write(to: exportsDirectory.appendingPathComponent("design.high-compat.pdf")) // Disabling high compatibility keeps complex elements as vectors. The export // is faster and the PDF is smaller, but rendering may differ across viewers. let standardOptions = ExportOptions(exportPdfWithHighCompatibility: false) let standardPdf = try await engine.block.export(page, mimeType: .pdf, options: standardOptions) try standardPdf.write(to: exportsDirectory.appendingPathComponent("design.standard.pdf")) // Define the spot color that represents the underlayer ink before exporting. // The RGB values are a preview; the underlayer is rendered as a separation // referencing the spot color name in print software. engine.editor.setSpotColor(name: "RDG_WHITE", r: 0.8, g: 0.8, b: 0.8) // Generate an underlayer from the design contours filled with the spot color. // A negative `underlayerOffset` shrinks the underlayer inward so misaligned // print layers do not show visible white edges around design elements. let underlayerOptions = ExportOptions( exportPdfWithHighCompatibility: true, exportPdfWithUnderlayer: true, underlayerSpotColorName: "RDG_WHITE", underlayerOffset: -2.0, ) let underlayerPdf = try await engine.block.export(page, mimeType: .pdf, options: underlayerOptions) try underlayerPdf.write(to: exportsDirectory.appendingPathComponent("design.underlayer.pdf")) // `targetWidth` / `targetHeight` are pixel dimensions. Combined with the // scene DPI set above, they determine the physical print size — 2480×3508 // pixels at 300 DPI is A4 (210×297 mm). let sizedOptions = ExportOptions( targetWidth: 2480, targetHeight: 3508, exportPdfWithHighCompatibility: true, ) let sizedPdf = try await engine.block.export(page, mimeType: .pdf, options: sizedOptions) try sizedPdf.write(to: exportsDirectory.appendingPathComponent("design.a4.pdf")) } ``` ## Default PDF Color Behavior CE.SDK exports PDFs in RGB color space. CMYK or spot colors defined in your design convert to RGB during standard export. For CMYK output with embedded ICC profiles, use the **Print Ready PDF plugin** described below — the plugin is a JavaScript package, so CMYK post-processing runs in a Node.js or browser pipeline that consumes the PDF emitted from your Swift app. The base `engine.block.export(_:mimeType:options:)` call provides the print compatibility options covered here, while full CMYK conversion is handled by the plugin. ## Setting Up for Print Export Configure the scene DPI before exporting. The DPI controls how complex elements are rasterized when high compatibility mode is enabled, and 300 DPI is the standard for high-quality print output. ```swift highlight-exportForPrinting-dpi // 300 DPI is standard for high-quality print output. try engine.block.setFloat(scene, property: "scene/dpi", value: 300) ``` Set the DPI on the scene block — not on the page — using the `scene/dpi` property. Every export from that scene then uses this resolution as the rasterization target. ## Exclusion Areas in a Print Export An exclusion area marks a region of a page that content must stay out of, such as an envelope window or a book spine. An exclusion area is authoring geometry, so it is left out of an export and its guide colors never reach the file. Set `exclusionArea/punchOut` to `true` on an exclusion area to cut it out of the export instead. The page and everything on it get a hole where the exclusion area is, so a die cut window in the design becomes a window in the exported PDF. The hole is transparent, which a print workflow reads as an absence of ink rather than as white. It is off by default. See [Exclusion Areas](https://img.ly/docs/cesdk/mac-catalyst/concepts/exclusion-areas-c2b87b/) for how to create and place an exclusion area. ## PDF Export Options for Print Export a page as PDF with `engine.block.export(_:mimeType:options:)`, passing `MIMEType.pdf` and an `ExportOptions` value. The PDF-specific fields on `ExportOptions` control how complex artwork is rendered. ### High Compatibility Mode `exportPdfWithHighCompatibility` defaults to `true` and rasterizes complex elements like gradients with transparency at the scene's DPI. Enable high compatibility when: - Designs use gradients with transparency - Effects or blend modes render inconsistently across PDF viewers - Maximum compatibility across print RIPs matters more than vector precision ```swift highlight-exportForPrinting-highCompatibility // High compatibility mode rasterizes complex elements like gradients with // transparency at the scene's DPI so they render consistently across PDF // viewers and print RIPs. let highCompatibilityOptions = ExportOptions(exportPdfWithHighCompatibility: true) let highCompatibilityPdf = try await engine.block.export( page, mimeType: .pdf, options: highCompatibilityOptions, ) try highCompatibilityPdf.write(to: exportsDirectory.appendingPathComponent("design.high-compat.pdf")) ``` The returned `Data` is a PDF blob you can write to disk, upload to a print service, or hand to a share sheet for the user to save. ### Standard PDF Export Disabling high compatibility keeps complex elements as vectors. The export is faster and the resulting PDF is smaller, but rendering may differ across viewers — useful when you target modern PDF viewers and prefer file size and speed over universal compatibility. ```swift highlight-exportForPrinting-standard // Disabling high compatibility keeps complex elements as vectors. The export // is faster and the PDF is smaller, but rendering may differ across viewers. let standardOptions = ExportOptions(exportPdfWithHighCompatibility: false) let standardPdf = try await engine.block.export(page, mimeType: .pdf, options: standardOptions) try standardPdf.write(to: exportsDirectory.appendingPathComponent("design.standard.pdf")) ``` In this mode CE.SDK embeds unmodified JPEG images with their original data instead of rasterizing them, which strongly reduces export time and file size for photo-heavy documents such as photo books. Print jobs usually keep the default `pdfImageQuality` of `1.0`, which encodes the images that still have to be rasterized losslessly. Lower the value only when a smaller file matters more than print fidelity. ## Underlayers for Special Media Underlayers provide a base ink layer (typically white) for printing on: - Transparent or non-white substrates - DTF (Direct-to-Film) transfers - Fabric, glass, or dark materials The underlayer is generated from the design's contours and filled with a named spot color. Print software then renders the underlayer as a separate ink separation that the press lays down before the design colors. ### Define the Underlayer Spot Color Before exporting with an underlayer, define the spot color that represents the underlayer ink. Use `engine.editor.setSpotColor(name:r:g:b:)` to create a named spot color with RGB preview values — the RGB triplet is only a screen approximation; the value the print process uses is the spot color name. ```swift highlight-exportForPrinting-defineSpotColor // Define the spot color that represents the underlayer ink before exporting. // The RGB values are a preview; the underlayer is rendered as a separation // referencing the spot color name in print software. engine.editor.setSpotColor(name: "RDG_WHITE", r: 0.8, g: 0.8, b: 0.8) ``` ### Export with Underlayer Set `exportPdfWithUnderlayer: true` and pass the spot color name as `underlayerSpotColorName`. The underlayer is generated from the design's contours and rendered as a fill of the named spot color. ```swift highlight-exportForPrinting-exportWithUnderlayer // Generate an underlayer from the design contours filled with the spot color. // A negative `underlayerOffset` shrinks the underlayer inward so misaligned // print layers do not show visible white edges around design elements. let underlayerOptions = ExportOptions( exportPdfWithHighCompatibility: true, exportPdfWithUnderlayer: true, underlayerSpotColorName: "RDG_WHITE", underlayerOffset: -2.0, ) let underlayerPdf = try await engine.block.export(page, mimeType: .pdf, options: underlayerOptions) try underlayerPdf.write(to: exportsDirectory.appendingPathComponent("design.underlayer.pdf")) ``` ### Underlayer Offset `underlayerOffset` adjusts the underlayer size in design units. Negative values shrink the underlayer inward, which prevents visible white edges when the print layers do not align perfectly. Start with values around `-1.0` to `-3.0` and tune based on your print equipment's alignment tolerance. ## Export with Target Size Control the exported PDF dimensions with `targetWidth` and `targetHeight`. These values are in pixels and combine with the scene's DPI to determine the physical print size — for example, 2480×3508 px at 300 DPI equals A4 (210×297 mm). ```swift highlight-exportForPrinting-targetSize // `targetWidth` / `targetHeight` are pixel dimensions. Combined with the // scene DPI set above, they determine the physical print size — 2480×3508 // pixels at 300 DPI is A4 (210×297 mm). let sizedOptions = ExportOptions( targetWidth: 2480, targetHeight: 3508, exportPdfWithHighCompatibility: true, ) let sizedPdf = try await engine.block.export(page, mimeType: .pdf, options: sizedOptions) try sizedPdf.write(to: exportsDirectory.appendingPathComponent("design.a4.pdf")) ``` When only one of `targetWidth` or `targetHeight` is non-zero, the engine scales the other axis to preserve the block's aspect ratio. When both are non-zero and the aspect ratios differ, the output fills the target dimensions completely and may exceed one of them on the longer axis. ## CMYK PDFs with ICC Profiles For CMYK color space and embedded ICC profiles, use the **Print Ready PDF plugin**. The plugin post-processes the PDF emitted by `engine.block.export(_:mimeType:options:)` and converts RGB to CMYK with embedded ICC profiles. The plugin is a JavaScript package, so wire it into a Node.js post-processing step or a browser-side pipeline that consumes the PDF produced by your Swift app. See the [Print Ready PDF Plugin](#broken-link-iroalu) for setup and usage. ## Troubleshooting ### PDF Not Opening Correctly in Print Software Pass `exportPdfWithHighCompatibility: true` so complex elements are rasterized at the scene DPI. Some prepress tools are strict about gradients with transparency and rendering effects that the standard PDF path keeps as vectors. ### Underlayer Not Visible in PDF Viewer Standard PDF viewers do not display spot color separations. Open the PDF in professional print software like Adobe Acrobat Pro or your prepress tool to verify that the underlayer separation is present. ### Colors Look Different After Printing Standard export uses RGB. Run the exported PDF through the Print Ready PDF plugin with an appropriate ICC profile when accurate CMYK reproduction is required. ### White Edges on Special Media Increase the negative `underlayerOffset` to shrink the underlayer further from design edges. Try values like `-2.0` or `-3.0` depending on your equipment's alignment tolerance. ## API Reference | Method/Option | Purpose | |---|---| | `engine.block.export(_:mimeType:options:)` | Export a block as PDF (or other format). | | `MIMEType.pdf` | PDF MIME type for the `mimeType` argument. | | `ExportOptions(targetWidth:)` | Target width for the exported PDF in pixels. | | `ExportOptions(targetHeight:)` | Target height for the exported PDF in pixels. | | `ExportOptions(exportPdfWithHighCompatibility:)` | Rasterize bitmap images, stripe fills, conical gradients, gradients with transparency and fills with effects at scene DPI (default: `true`). | | `ExportOptions(pdfImageQuality:)` | Encoding quality for rasterized images; values below `1.0` use lossy JPEG (default: `1.0`). | | `ExportOptions(exportPdfWithUnderlayer:)` | Generate underlayer from contours (default: `false`). | | `ExportOptions(underlayerSpotColorName:)` | Spot color name for underlayer ink. | | `ExportOptions(underlayerOffset:)` | Size adjustment in design units (negative shrinks). | | `engine.editor.setSpotColor(name:r:g:b:)` | Define a spot color from RGB values. | | `engine.block.setFloat(_:property:value:)` with `"scene/dpi"` | Set scene DPI for print resolution. | ## Next Steps - [CMYK Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/cmyk-8a1334/) — Configure CMYK colors - [Spot Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/for-print/spot-c3a150/) — Define and use spot colors - [Export to PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) — General PDF export options --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Export for Social Media" description: "Export vertical videos with the correct dimensions, formats, and quality settings for Instagram Reels, TikTok, and YouTube Shorts." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-social-media-0e8a92/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [For Social Media](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-social-media-0e8a92/) --- Export vertical video designs for social media platforms with the correct dimensions, formats, and quality settings. Configure video exports with appropriate resolution, framerate, and bitrate optimized for Instagram Reels, TikTok, and YouTube Shorts. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-export-for-social-media) Short-form vertical video has become the dominant format for social media. Instagram Reels, TikTok, and YouTube Shorts all use the 9:16 aspect ratio at 1080×1920 pixels. This guide demonstrates how to create and export vertical video content with the correct settings for these platforms. ```swift file=@cesdk_swift_examples/engine-guides-export-for-social-media/ForSocialMedia.swift reference-only import Foundation import IMGLYEngine @MainActor func forSocialMedia(engine: Engine) async throws { let scene = try engine.scene.createVideo() try engine.scene.setDesignUnit(.px) let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1080) try engine.block.setHeight(page, value: 1920) let baseURL = try engine.guidesBaseURL let videoBlock = try engine.block.create(.graphic) try engine.block.setShape(videoBlock, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(videoBlock, fill: videoFill) try engine.block.appendChild(to: page, child: videoBlock) try engine.block.fillParent(videoBlock) try await engine.block.forceLoadAVResource(videoFill) let options = VideoExportOptions( videoBitrate: 8_000_000, // 8 Mbps framerate: 30, targetWidth: 1080, targetHeight: 1920, ) var exportedVideo: Blob? for try await event in try await engine.block.exportVideo( page, mimeType: .mp4, options: options, ) { switch event { case let .progress(renderedFrames, encodedFrames, totalFrames): let percent = totalFrames == 0 ? 0 : Int((Double(encodedFrames) / Double(totalFrames)) * 100) print("Export \(percent)% – rendered \(renderedFrames), encoded \(encodedFrames) of \(totalFrames)") case let .finished(video: blob): exportedVideo = blob } } guard let videoData = exportedVideo else { return } let outputURL = FileManager.default.temporaryDirectory .appendingPathComponent("vertical-video-1080x1920.mp4") try videoData.write(to: outputURL) } ``` This guide covers creating a vertical video scene, exporting with resolution, framerate, and bitrate settings, and tracking export progress. ## Creating a Scene Create a video scene and pin its design unit to pixels so the page dimensions you set match the platform requirements exactly. Add a page sized 1080×1920 (9:16), the standard resolution for Instagram Reels, TikTok, and YouTube Shorts. ```swift highlight-forSocialMedia-createScene let scene = try engine.scene.createVideo() try engine.scene.setDesignUnit(.px) let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1080) try engine.block.setHeight(page, value: 1920) ``` `engine.scene.createVideo()` returns the new scene block and switches the engine into video mode. `setDesignUnit(.px)` makes subsequent `setWidth`/`setHeight` calls operate in pixels regardless of the scene's previous unit. ## Adding a Video Fill the page with a video clip so the export has visible content. Create a graphic block with a rectangle shape, attach a video fill backed by a remote URL, append the block to the page, and call `fillParent` so it covers the full 1080×1920 frame. ```swift highlight-forSocialMedia-addVideo let videoBlock = try engine.block.create(.graphic) try engine.block.setShape(videoBlock, shape: engine.block.createShape(.rect)) let videoFill = try engine.block.createFill(.video) let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(videoBlock, fill: videoFill) try engine.block.appendChild(to: page, child: videoBlock) try engine.block.fillParent(videoBlock) try await engine.block.forceLoadAVResource(videoFill) ``` `forceLoadAVResource(videoFill)` blocks until the remote video is downloaded and parsed. Calling it before export keeps the export pipeline from waiting on resource I/O while frames are being encoded. ## Configuring Export Options `VideoExportOptions` controls resolution, framerate, and bitrate. For Instagram Reels, TikTok, and YouTube Shorts, render to 1080×1920 at 30 frames per second with an 8 Mbps video bitrate. ```swift highlight-forSocialMedia-exportOptions let options = VideoExportOptions( videoBitrate: 8_000_000, // 8 Mbps framerate: 30, targetWidth: 1080, targetHeight: 1920, ) ``` Key video export settings: - **targetWidth / targetHeight**: Output resolution (1080×1920 for vertical) - **framerate**: 30 frames per second (standard for social media) - **videoBitrate**: 8 Mbps balances quality and upload speed for short-form video Higher bitrates produce better quality but larger files. For an automatic video bitrate, pass `VideoBitrate.auto` (`-1`) for a bounded, resolution-aware value, or the default `VideoBitrate.system` (`0`) to let the platform encoder choose. Pass `0` to `audioBitrate` to let the engine pick the audio bitrate automatically. ## Exporting Videos `engine.block.exportVideo(_:mimeType:options:)` returns an `AsyncThrowingStream` that yields progress events while the export runs and a final `.finished(video:)` event carrying the encoded MP4 as a `Blob` (a typealias for `Data`). Use `MIMEType.mp4` for broad platform compatibility. ```swift highlight-forSocialMedia-exportVideo var exportedVideo: Blob? for try await event in try await engine.block.exportVideo( page, mimeType: .mp4, options: options, ) { switch event { case let .progress(renderedFrames, encodedFrames, totalFrames): let percent = totalFrames == 0 ? 0 : Int((Double(encodedFrames) / Double(totalFrames)) * 100) print("Export \(percent)% – rendered \(renderedFrames), encoded \(encodedFrames) of \(totalFrames)") case let .finished(video: blob): exportedVideo = blob } } guard let videoData = exportedVideo else { return } ``` The `for try await` loop drains the stream until the export completes. Capture the final blob in an optional and unwrap it before saving so an interrupted export doesn't write a zero-byte file. ## Tracking Export Progress The `.progress` case fires repeatedly during the export and reports three frame counts: - **renderedFrames** – Frames the engine has rendered so far. - **encodedFrames** – Frames the encoder has written to the output stream. - **totalFrames** – Total frames the export will produce. ```swift highlight-forSocialMedia-progress case let .progress(renderedFrames, encodedFrames, totalFrames): let percent = totalFrames == 0 ? 0 : Int((Double(encodedFrames) / Double(totalFrames)) * 100) print("Export \(percent)% – rendered \(renderedFrames), encoded \(encodedFrames) of \(totalFrames)") ``` Encoding trails rendering slightly. Drive a progress indicator from `encodedFrames / totalFrames` for an accurate completion percentage. `totalFrames` can be `0` for the first event or two — guard against division by zero before computing a percentage. ## Saving the Exported Video The `.finished` case yields a `Blob` containing the MP4 bytes. Write it to a file with `Data.write(to:)` to hand it off to a share sheet, an upload pipeline, or the photo library. ```swift highlight-forSocialMedia-saveFile let outputURL = FileManager.default.temporaryDirectory .appendingPathComponent("vertical-video-1080x1920.mp4") try videoData.write(to: outputURL) ``` For uploads, pass the `Blob` directly to your networking layer instead of writing it to disk first. ## API Reference | Method | Purpose | |--------|---------| | `engine.scene.createVideo()` | Create a video scene | | `engine.scene.setDesignUnit(_:)` | Pin the scene's design unit (`.px`, `.mm`, `.in`) | | `engine.block.fillParent(_:)` | Resize a block to fill its parent | | `engine.block.forceLoadAVResource(_:)` | Wait for a video fill's source to load | | `engine.block.exportVideo(_:mimeType:options:)` | Export a block as a video stream | ### Export Options (Videos) | Option | Type | Description | |--------|------|-------------| | `targetWidth` | `Float` | Output width in design units | | `targetHeight` | `Float` | Output height in design units | | `framerate` | `Float?` | Frames per second (default: the scene's `scene/frameRate`) | | `videoBitrate` | `Int32` | Video bitrate in bits per second, or `VideoBitrate.system` (`0`, default) / `VideoBitrate.auto` (`-1`) | | `audioBitrate` | `Int32` | Audio bitrate in bits per second (`0` = auto) | | `h264Profile` | `H264Profile` | Encoder feature set: `.baseline`, `.extended`, `.main` (default), `.high` | | `h264Level` | `Int32` | H.264 level × 10 (default `52` = level 5.2) | | `timeOffset` | `Double` | Start time in seconds (default `0`) | | `duration` | `Double` | Output duration in seconds (`0` = full scene) | ## Next Steps - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) - Complete export options including H.264 profiles and advanced settings --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Pre-Export Validation" description: "Documentation for Pre-Export Validation" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/pre-export-validation-3a2cba/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [Pre-Export Validation](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/pre-export-validation-3a2cba/) --- Validate a design before export by detecting elements outside the page, protruding content, obscured text, and unfilled placeholders. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-pre-export-validation) Pre-export validation catches layout and content issues before export, preventing problems like cropped content, hidden text, and incomplete designs in the final output. The checks shown here run entirely against `IMGLYEngine` block APIs, so they work the same in any export pipeline. ```swift file=@cesdk_swift_examples/engine-guides-pre-export-validation/PreExportValidation.swift reference-only import Foundation import IMGLYEngine struct BoundingBox { let minX: Float let minY: Float let maxX: Float let maxY: Float } enum ValidationSeverity { case error case warning } struct ValidationIssue { enum Kind { case outsidePage case protruding case textObscured case unfilledPlaceholder } let kind: Kind let severity: ValidationSeverity let blockID: DesignBlockID let blockName: String let message: String } struct ValidationResult { let errors: [ValidationIssue] let warnings: [ValidationIssue] } // Display name with a kind-based fallback used in issue messages. @MainActor private func displayName(engine: Engine, _ blockID: DesignBlockID) throws -> String { let name = try engine.block.getName(blockID) if !name.isEmpty { return name } let kind = try engine.block.getKind(blockID) return kind.prefix(1).uppercased() + kind.dropFirst() } @MainActor private func boundingBox(engine: Engine, _ blockID: DesignBlockID) throws -> BoundingBox { let x = try engine.block.getGlobalBoundingBoxX(blockID) let y = try engine.block.getGlobalBoundingBoxY(blockID) let width = try engine.block.getGlobalBoundingBoxWidth(blockID) let height = try engine.block.getGlobalBoundingBoxHeight(blockID) return BoundingBox(minX: x, minY: y, maxX: x + width, maxY: y + height) } // Returns the fraction of `box1` that intersects `box2` (0 = none, 1 = fully inside). private func overlapRatio(_ box1: BoundingBox, _ box2: BoundingBox) -> Float { let intersectWidth = max(0, min(box1.maxX, box2.maxX) - max(box1.minX, box2.minX)) let intersectHeight = max(0, min(box1.maxY, box2.maxY) - max(box1.minY, box2.minY)) let box1Area = (box1.maxX - box1.minX) * (box1.maxY - box1.minY) return box1Area == 0 ? 0 : (intersectWidth * intersectHeight) / box1Area } @MainActor private func findOutsideBlocks(engine: Engine, page: DesignBlockID) throws -> [ValidationIssue] { var issues: [ValidationIssue] = [] let pageBounds = try boundingBox(engine: engine, page) let candidates = try engine.block.find(byType: .text) + engine.block.find(byType: .graphic) for blockID in candidates where engine.block.isValid(blockID) { let blockBounds = try boundingBox(engine: engine, blockID) if overlapRatio(blockBounds, pageBounds) == 0 { issues.append(ValidationIssue( kind: .outsidePage, severity: .error, blockID: blockID, blockName: try displayName(engine: engine, blockID), message: "Element is completely outside the visible page area", )) } } return issues } @MainActor private func findProtrudingBlocks(engine: Engine, page: DesignBlockID) throws -> [ValidationIssue] { var issues: [ValidationIssue] = [] let pageBounds = try boundingBox(engine: engine, page) let candidates = try engine.block.find(byType: .text) + engine.block.find(byType: .graphic) for blockID in candidates where engine.block.isValid(blockID) { let blockBounds = try boundingBox(engine: engine, blockID) let overlap = overlapRatio(blockBounds, pageBounds) // Partially inside (> 0) but not fully inside (< 1). if overlap > 0, overlap < 0.99 { issues.append(ValidationIssue( kind: .protruding, severity: .warning, blockID: blockID, blockName: try displayName(engine: engine, blockID), message: "Element extends beyond page boundaries", )) } } return issues } @MainActor private func findObscuredText(engine: Engine, page: DesignBlockID) throws -> [ValidationIssue] { var issues: [ValidationIssue] = [] let children = try engine.block.getChildren(page) let textBlocks = try engine.block.find(byType: .text) for textID in textBlocks where engine.block.isValid(textID) { guard let textIndex = children.firstIndex(of: textID) else { continue } // Children later in the array are rendered on top. let blocksAbove = children[(textIndex + 1)...] let textBounds = try boundingBox(engine: engine, textID) for aboveID in blocksAbove { // Skip text-on-text overlaps — text backgrounds are typically transparent. if try engine.block.getType(aboveID) == DesignBlockType.text.rawValue { continue } if try overlapRatio(textBounds, boundingBox(engine: engine, aboveID)) > 0 { issues.append(ValidationIssue( kind: .textObscured, severity: .warning, blockID: textID, blockName: try displayName(engine: engine, textID), message: "Text may be partially hidden by overlapping elements", )) break } } } return issues } @MainActor private func findUnfilledPlaceholders(engine: Engine) throws -> [ValidationIssue] { var issues: [ValidationIssue] = [] for blockID in engine.block.findAllPlaceholders() where engine.block.isValid(blockID) { if try !isPlaceholderFilled(engine: engine, blockID) { issues.append(ValidationIssue( kind: .unfilledPlaceholder, severity: .error, blockID: blockID, blockName: try displayName(engine: engine, blockID), message: "Placeholder has not been filled with content", )) } } return issues } @MainActor private func isPlaceholderFilled(engine: Engine, _ blockID: DesignBlockID) throws -> Bool { let fillID = try engine.block.getFill(blockID) guard engine.block.isValid(fillID) else { return false } // Empty `fill/image/imageFileURI` means the image placeholder has not been filled. if try engine.block.getType(fillID) == FillType.image.rawValue { let uri = try engine.block.getString(fillID, property: "fill/image/imageFileURI") return !uri.isEmpty } // Other fill types are treated as filled. return true } @MainActor func preExportValidation(engine: Engine) async throws { // The block below builds a demo scene that triggers every validation check. // It is not part of the guide content — readers integrate the helpers above // into their own scenes and export pipelines. let scene = try engine.scene.create() let pageID = try engine.block.create(.page) try engine.block.setWidth(pageID, value: 800) try engine.block.setHeight(pageID, value: 600) try engine.block.appendChild(to: scene, child: pageID) let baseURL = try engine.guidesBaseURL try addValidationDemoBlocks(engine: engine, page: pageID, baseURL: baseURL) let allIssues = try findOutsideBlocks(engine: engine, page: pageID) + findProtrudingBlocks(engine: engine, page: pageID) + findObscuredText(engine: engine, page: pageID) + findUnfilledPlaceholders(engine: engine) let result = ValidationResult( errors: allIssues.filter { $0.severity == .error }, warnings: allIssues.filter { $0.severity == .warning }, ) if let firstError = result.errors.first, engine.block.isValid(firstError.blockID) { // Select the first error block to help the user locate the issue. try engine.block.select(firstError.blockID) } // Suppress unused-variable warning for the demo summary. _ = result.warnings } // MARK: - Demo scene scaffolding (not part of the guide) @MainActor private func addValidationDemoBlocks(engine: Engine, page: DesignBlockID, baseURL: URL) throws { try addOutsideImage(engine: engine, page: page, baseURL: baseURL) try addProtrudingImage(engine: engine, page: page, baseURL: baseURL) try addObscuredTextWithOverlap(engine: engine, page: page) try addUnfilledPlaceholder(engine: engine, page: page) } @MainActor private func addOutsideImage(engine: Engine, page: DesignBlockID, baseURL: URL) throws { let block = try engine.block.create(.graphic) try engine.block.setName(block, name: "Outside Image") try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.image) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: 150) try engine.block.setHeight(block, value: 100) try engine.block.setPositionX(block, value: -200) try engine.block.setPositionY(block, value: 100) try engine.block.appendChild(to: page, child: block) } @MainActor private func addProtrudingImage(engine: Engine, page: DesignBlockID, baseURL: URL) throws { let block = try engine.block.create(.graphic) try engine.block.setName(block, name: "Protruding Image") try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.image) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: 150) try engine.block.setHeight(block, value: 100) try engine.block.setPositionX(block, value: 725) try engine.block.setPositionY(block, value: 100) try engine.block.appendChild(to: page, child: block) } @MainActor private func addObscuredTextWithOverlap(engine: Engine, page: DesignBlockID) throws { let text = try engine.block.create(.text) try engine.block.setName(text, name: "Obscured Text") try engine.block.setPositionX(text, value: 200) try engine.block.setPositionY(text, value: 250) try engine.block.setWidth(text, value: 200) try engine.block.setHeight(text, value: 100) try engine.block.replaceText(text, text: "Hidden") try engine.block.appendChild(to: page, child: text) // Overlapping shape rendered above the text (later in stacking order). let shape = try engine.block.create(.graphic) try engine.block.setName(shape, name: "Overlapping Shape") try engine.block.setShape(shape, shape: engine.block.createShape(.rect)) try engine.block.setFill(shape, fill: engine.block.createFill(.color)) try engine.block.setPositionX(shape, value: 200) try engine.block.setPositionY(shape, value: 250) try engine.block.setWidth(shape, value: 200) try engine.block.setHeight(shape, value: 100) try engine.block.appendChild(to: page, child: shape) } @MainActor private func addUnfilledPlaceholder(engine: Engine, page: DesignBlockID) throws { let block = try engine.block.create(.graphic) try engine.block.setName(block, name: "Unfilled Placeholder") try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.image) try engine.block.setFill(block, fill: fill) try engine.block.setWidth(block, value: 150) try engine.block.setHeight(block, value: 100) try engine.block.setPositionX(block, value: 50) try engine.block.setPositionY(block, value: 400) try engine.block.appendChild(to: page, child: block) try engine.block.setScopeEnabled(block, key: "fill/change", enabled: true) try engine.block.setPlaceholderBehaviorEnabled(fill, enabled: true) try engine.block.setPlaceholderEnabled(block, enabled: true) } ``` Each check returns `ValidationIssue` values categorized by severity. Errors describe content that won't appear correctly in the export; warnings describe content that may not appear as intended. The shared issue model and the helper that returns block bounds anchor every check. ```swift highlight-types struct BoundingBox { let minX: Float let minY: Float let maxX: Float let maxY: Float } enum ValidationSeverity { case error case warning } struct ValidationIssue { enum Kind { case outsidePage case protruding case textObscured case unfilledPlaceholder } let kind: Kind let severity: ValidationSeverity let blockID: DesignBlockID let blockName: String let message: String } struct ValidationResult { let errors: [ValidationIssue] let warnings: [ValidationIssue] } // Display name with a kind-based fallback used in issue messages. @MainActor private func displayName(engine: Engine, _ blockID: DesignBlockID) throws -> String { let name = try engine.block.getName(blockID) if !name.isEmpty { return name } let kind = try engine.block.getKind(blockID) return kind.prefix(1).uppercased() + kind.dropFirst() } ``` ## Getting Element Bounds Every check compares positions in global coordinates. `getGlobalBoundingBox{X,Y,Width,Height}` accounts for all transformations, so wrap them in a small helper that returns `(minX, minY, maxX, maxY)`. The overlap helper divides the intersection area by the first box's area, producing a ratio from `0` (fully outside) to `1` (fully inside). ```swift highlight-getBoundingBox @MainActor private func boundingBox(engine: Engine, _ blockID: DesignBlockID) throws -> BoundingBox { let x = try engine.block.getGlobalBoundingBoxX(blockID) let y = try engine.block.getGlobalBoundingBoxY(blockID) let width = try engine.block.getGlobalBoundingBoxWidth(blockID) let height = try engine.block.getGlobalBoundingBoxHeight(blockID) return BoundingBox(minX: x, minY: y, maxX: x + width, maxY: y + height) } // Returns the fraction of `box1` that intersects `box2` (0 = none, 1 = fully inside). private func overlapRatio(_ box1: BoundingBox, _ box2: BoundingBox) -> Float { let intersectWidth = max(0, min(box1.maxX, box2.maxX) - max(box1.minX, box2.minX)) let intersectHeight = max(0, min(box1.maxY, box2.maxY) - max(box1.minY, box2.minY)) let box1Area = (box1.maxX - box1.minX) * (box1.maxY - box1.minY) return box1Area == 0 ? 0 : (intersectWidth * intersectHeight) / box1Area } ``` ## Detecting Elements Outside the Page Elements completely outside the page are missing from the export. Iterate over the relevant block types — `text` and `graphic` — and flag any block whose overlap with the page is zero. ```swift highlight-findOutsideBlocks @MainActor private func findOutsideBlocks(engine: Engine, page: DesignBlockID) throws -> [ValidationIssue] { var issues: [ValidationIssue] = [] let pageBounds = try boundingBox(engine: engine, page) let candidates = try engine.block.find(byType: .text) + engine.block.find(byType: .graphic) for blockID in candidates where engine.block.isValid(blockID) { let blockBounds = try boundingBox(engine: engine, blockID) if overlapRatio(blockBounds, pageBounds) == 0 { issues.append(ValidationIssue( kind: .outsidePage, severity: .error, blockID: blockID, blockName: try displayName(engine: engine, blockID), message: "Element is completely outside the visible page area", )) } } return issues } ``` This is treated as an error because the content will not appear at all. ## Detecting Protruding Elements Elements that are partially inside the page get cropped on export. The same per-block scan flags any overlap that is greater than `0` but less than `1`. A small tolerance (`< 0.99`) avoids false positives from sub-pixel rounding. ```swift highlight-findProtrudingBlocks @MainActor private func findProtrudingBlocks(engine: Engine, page: DesignBlockID) throws -> [ValidationIssue] { var issues: [ValidationIssue] = [] let pageBounds = try boundingBox(engine: engine, page) let candidates = try engine.block.find(byType: .text) + engine.block.find(byType: .graphic) for blockID in candidates where engine.block.isValid(blockID) { let blockBounds = try boundingBox(engine: engine, blockID) let overlap = overlapRatio(blockBounds, pageBounds) // Partially inside (> 0) but not fully inside (< 1). if overlap > 0, overlap < 0.99 { issues.append(ValidationIssue( kind: .protruding, severity: .warning, blockID: blockID, blockName: try displayName(engine: engine, blockID), message: "Element extends beyond page boundaries", )) } } return issues } ``` These are warnings — the content is still partially visible, but may not look as intended. ## Finding Obscured Text Text hidden behind other elements is hard to read. `getChildren()` returns blocks in stacking order — elements later in the array render on top. For each text block, check whether any non-text element above it overlaps its bounds. ```swift highlight-findObscuredText @MainActor private func findObscuredText(engine: Engine, page: DesignBlockID) throws -> [ValidationIssue] { var issues: [ValidationIssue] = [] let children = try engine.block.getChildren(page) let textBlocks = try engine.block.find(byType: .text) for textID in textBlocks where engine.block.isValid(textID) { guard let textIndex = children.firstIndex(of: textID) else { continue } // Children later in the array are rendered on top. let blocksAbove = children[(textIndex + 1)...] let textBounds = try boundingBox(engine: engine, textID) for aboveID in blocksAbove { // Skip text-on-text overlaps — text backgrounds are typically transparent. if try engine.block.getType(aboveID) == DesignBlockType.text.rawValue { continue } if try overlapRatio(textBounds, boundingBox(engine: engine, aboveID)) > 0 { issues.append(ValidationIssue( kind: .textObscured, severity: .warning, blockID: textID, blockName: try displayName(engine: engine, textID), message: "Text may be partially hidden by overlapping elements", )) break } } } return issues } ``` Text-on-text comparisons are skipped because text backgrounds are typically transparent and rarely obscure other text. ## Checking Placeholder Content Placeholders mark areas the user must fill before export. `findAllPlaceholders()` returns every placeholder block in the design. For each one, look up its fill and decide whether it has been filled. ```swift highlight-findUnfilledPlaceholders @MainActor private func findUnfilledPlaceholders(engine: Engine) throws -> [ValidationIssue] { var issues: [ValidationIssue] = [] for blockID in engine.block.findAllPlaceholders() where engine.block.isValid(blockID) { if try !isPlaceholderFilled(engine: engine, blockID) { issues.append(ValidationIssue( kind: .unfilledPlaceholder, severity: .error, blockID: blockID, blockName: try displayName(engine: engine, blockID), message: "Placeholder has not been filled with content", )) } } return issues } @MainActor private func isPlaceholderFilled(engine: Engine, _ blockID: DesignBlockID) throws -> Bool { let fillID = try engine.block.getFill(blockID) guard engine.block.isValid(fillID) else { return false } // Empty `fill/image/imageFileURI` means the image placeholder has not been filled. if try engine.block.getType(fillID) == FillType.image.rawValue { let uri = try engine.block.getString(fillID, property: "fill/image/imageFileURI") return !uri.isEmpty } // Other fill types are treated as filled. return true } ``` For image placeholders, an empty `fill/image/imageFileURI` means the placeholder still needs content. Other fill types are treated as filled. Unfilled placeholders are errors that should block export so the user is forced to complete the design. ## Running Validation Locate the page block (typically `try engine.block.find(byType: .page).first!`) and aggregate the four checks into a single `ValidationResult`, separating errors from warnings. When errors exist, select the first problematic block so the user can locate it immediately. Integrate this orchestrator into your export pipeline — block export when `result.errors` is non-empty, surface `result.warnings` as a confirmation prompt, and proceed otherwise. ```swift highlight-validateDesign let allIssues = try findOutsideBlocks(engine: engine, page: pageID) + findProtrudingBlocks(engine: engine, page: pageID) + findObscuredText(engine: engine, page: pageID) + findUnfilledPlaceholders(engine: engine) let result = ValidationResult( errors: allIssues.filter { $0.severity == .error }, warnings: allIssues.filter { $0.severity == .warning }, ) if let firstError = result.errors.first, engine.block.isValid(firstError.blockID) { // Select the first error block to help the user locate the issue. try engine.block.select(firstError.blockID) } ``` ## API Reference | Method | Purpose | | --- | --- | | `engine.block.getGlobalBoundingBoxX(id)` | Get the element's global X position | | `engine.block.getGlobalBoundingBoxY(id)` | Get the element's global Y position | | `engine.block.getGlobalBoundingBoxWidth(id)` | Get the element's global width | | `engine.block.getGlobalBoundingBoxHeight(id)` | Get the element's global height | | `engine.block.find(byType:)` | Find all blocks of a specific type | | `engine.block.getChildren(id)` | Get child blocks in stacking order | | `engine.block.getType(id)` | Get the block's type string | | `engine.block.getName(id)` | Get the block's display name | | `engine.block.getKind(id)` | Get the block's kind | | `engine.block.isValid(id)` | Check whether the block exists | | `engine.block.select(id)` | Select a block in the editor | | `engine.block.findAllPlaceholders()` | Find all placeholder blocks | | `engine.block.getFill(id)` | Get the fill block for a given block | | `engine.block.getString(id, property:)` | Read a string property value | ## Next Steps - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Learn about the available export formats and options - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Understand the block hierarchy and positioning model --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Save" description: "Save design progress locally or to a backend service to allow for later editing or publishing." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Save](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) --- Save and serialize designs in CE.SDK for later retrieval, sharing, or storage using string or archive formats. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-save-designs) CE.SDK provides two formats for persisting designs. Choose the format based on your storage and portability requirements. ```swift file=@cesdk_swift_examples/engine-guides-save-designs/SaveDesigns.swift reference-only import Foundation import IMGLYEngine @MainActor func saveDesigns(engine: Engine) async throws { // Demo scaffolding: load a template so every snippet has a scene to operate on. // In your app you would start from a scene already loaded into the editor. let baseURL = try engine.guidesBaseURL try engine.editor.setSettingString("basePath", value: baseURL.absoluteString) let templateURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: templateURL) let outputDir = FileManager.default.temporaryDirectory let sceneString = try await engine.scene.saveToString() // The default schemes have no "data", so a data: URI fails the save. // Add "data" to keep data: URIs in the string. let sceneWithDataURIs = try await engine.scene.saveToString( allowedResourceSchemes: ["blob", "bundle", "data", "file", "http", "https"], ) _ = sceneWithDataURIs let archiveBlob = try await engine.scene.saveToArchive() let compressed = try await engine.scene.saveToString( options: SaveToStringOptions( compression: CompressionOptions(format: .zstd, level: .default), ), ) _ = compressed let compressedArchive = try await engine.scene.saveToArchive( options: SaveToArchiveOptions( compression: CompressionOptions(format: .zstd, level: .default), ), ) _ = compressedArchive let sceneURL = outputDir.appendingPathComponent("scene.imgly") try sceneString.write(to: sceneURL, atomically: true, encoding: .utf8) let archiveURL = outputDir.appendingPathComponent("archive.imgly") try archiveBlob.write(to: archiveURL) let restoredString = try String(contentsOf: sceneURL, encoding: .utf8) try await engine.scene.load(from: restoredString) try await engine.scene.load(from: archiveURL) } ``` ## Save Format Comparison | Format | Method | Assets | Best For | | ------- | ----------------- | ----------------- | ---------------------------- | | String | `saveToString()` | Referenced by URL | Database storage, cloud sync | | Archive | `saveToArchive()` | Embedded in ZIP | Offline use, file sharing | **String format** produces a lightweight serialized string where assets remain as URL references. Use this when asset URLs will remain accessible. **Archive format** creates a self-contained ZIP with all assets embedded. Use this for portable designs that work offline. Persist saved files of either format with the `.imgly` extension. The `.scene` and `.zip` extensions also load, and the same `engine.scene.load(from:)` call opens either kind. ### Archive Preview Image Scene and block archives containing a page automatically include a root `preview.png` as the first ZIP entry. The PNG is stored without ZIP compression. Its local ZIP header records its byte length. Clients can read the preview without fetching the document, resources, or central directory. It shows the first exported page at its playback position when saving starts (0 for static scenes). Video pages use the exact frame at that captured time. Scene archives use scene page order. Block archives use export-root order, then depth-first hierarchy order. The PNG preserves the page background and transparency. Preview generation starts after every archive resource has loaded, and uses fast lossless PNG compression. Both dimensions are capped at 512 px. Pages that fit within 512 × 512 px keep their dimensions without upscaling. Larger pages are scaled down proportionally: a 1920 × 1080 page produces 512 × 288 px, while a 320 × 180 page stays 320 × 180 px. Larger outputs may be resampled from the thumbnail pipeline's internal render resolution. Rendering or encoding failures and unsupported dimensions cause the engine to omit the preview. Archives without a page also omit it. The engine ignores the exact root `preview.png` when loading and regenerates it on each archive save. External tools can extract this image from the ZIP without loading the design. File explorers require a compatible preview handler to display it automatically. ## Save to String Serialize the current scene to a string suitable for database storage. ```swift highlight-saveDesigns-saveToString let sceneString = try await engine.scene.saveToString() ``` The string contains the complete scene structure but references assets by their original URLs. The default `allowedResourceSchemes` do not include `data`, so a scene that holds a `data:` URI fails to save with `SCENE.DISALLOWED_SCHEMES`. Add `"data"` to keep those URIs in the string: ```swift highlight-saveDesigns-saveWithDataURIs // The default schemes have no "data", so a data: URI fails the save. // Add "data" to keep data: URIs in the string. let sceneWithDataURIs = try await engine.scene.saveToString( allowedResourceSchemes: ["blob", "bundle", "data", "file", "http", "https"], ) ``` An archive needs no option: it bundles every resource, `data:` URIs included. ## Save to Archive Create a self-contained ZIP with the scene and all embedded assets. ```swift highlight-saveDesigns-saveToArchive let archiveBlob = try await engine.scene.saveToArchive() ``` `saveToArchive()` returns a `Blob` (a `Data` value) that includes all pages, elements, and asset data in a single portable file. ## Compare Saved Scenes Saving writes the scene as it is laid out at that moment. While a font loads, its text is laid out with another font. When the size of a text follows its content, a scene saved before the font arrives can differ from the same scene saved after it. To compare two saved scenes, for example by a hash, load the fonts first with `try await engine.block.forceLoadResources([engine.scene.get()!])`. The call throws when a resource fails to load, and you can still save the scene then. To compare two archives, compare the `scene.scene` files inside them, because each archive entry stores the time of the save. ## Compression Options Saved scenes are compressed with Zstd by default, which makes them much smaller and speeds up both saving and loading. Pass a format explicitly to change the level, or to turn compression off. ```swift highlight-saveDesigns-compression let compressed = try await engine.scene.saveToString( options: SaveToStringOptions( compression: CompressionOptions(format: .zstd, level: .default), ), ) ``` **Compression Formats:** - `CompressionFormat.zstd` — Zstd compression (default) - `CompressionFormat.none` — No compression **Compression Levels:** - `CompressionLevel.fastest` — Fastest compression, larger output - `CompressionLevel.default` — Balanced speed and size (recommended) - `CompressionLevel.best` — Best compression, slower An archive can compress its scene the same way. Bundled images, video and fonts are stored as they are, because they already are compressed formats. ```swift highlight-saveDesigns-archiveCompression let compressedArchive = try await engine.scene.saveToArchive( options: SaveToArchiveOptions( compression: CompressionOptions(format: .zstd, level: .default), ), ) ``` Compression adds minimal overhead while reducing scene size. The default level provides the best balance of speed and compression ratio. ## Write to Disk Use Foundation's file APIs to persist saved designs to the file system. Scene strings can be written directly as text: ```swift highlight-saveDesigns-writeScene let sceneURL = outputDir.appendingPathComponent("scene.imgly") try sceneString.write(to: sceneURL, atomically: true, encoding: .utf8) ``` Archives are returned as `Data`, which writes to disk in a single call: ```swift highlight-saveDesigns-writeArchive let archiveURL = outputDir.appendingPathComponent("archive.imgly") try archiveBlob.write(to: archiveURL) ``` ## Load Scene from File Read a previously saved `.scene` file from disk and restore it to the engine with `engine.scene.load(from:)`. ```swift highlight-saveDesigns-loadScene let restoredString = try String(contentsOf: sceneURL, encoding: .utf8) try await engine.scene.load(from: restoredString) ``` Scene files are lightweight but require the original asset URLs to remain accessible. ## Load Archive from File Use `engine.scene.load(from:)` with a local file URL to restore a self-contained archive that includes all embedded assets — the same call that loads scene files, since the engine detects the file kind automatically. ```swift highlight-saveDesigns-loadArchive try await engine.scene.load(from: archiveURL) ``` Archives are portable and work offline since all assets are bundled within the file. ## API Reference | Method | Description | | ------------------------------------- | ------------------------------------------------------------------------------- | | `engine.scene.saveToString()` | Serialize scene to string with optional compression | | `engine.scene.saveToArchive()` | Save scene with assets as ZIP `Data` | | `engine.scene.load(from:)` | Load a scene or archive from a string or URL (file kind detected automatically) | | `engine.scene.loadArchive(from:)` | Load a scene archive from a URL | | `engine.block.saveToString()` | Serialize specific blocks to a string | | `engine.block.saveToArchive(blocks:)` | Save specific blocks with assets as ZIP `Data` | | `engine.block.load(from:)` | Load blocks from a serialized string or URL | | `engine.block.loadArchive(from:)` | Load blocks from a ZIP archive URL | ## Next Steps - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Export designs to image, PDF, and video formats - [Load Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/) — Load scenes from remote URLs and archives - [Store Custom Metadata](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/store-custom-metadata-337248/) — Attach metadata like tags or version info to designs - [Partial Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/partial-export-89aaf6/) — Export individual blocks or selections --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Store Custom Metadata" description: "Attach, retrieve, and manage custom key-value metadata on design blocks in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/store-custom-metadata-337248/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Store Custom Metadata](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/store-custom-metadata-337248/) --- Attach custom key-value metadata to design blocks in CE.SDK for tracking asset origins, storing application state, or linking to external systems. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-store-custom-metadata) Metadata lets you attach arbitrary string key-value pairs to any design block. The data is invisible to end users but persists with the scene through save and load operations. Common use cases include tracking asset origins, storing application-specific state, and linking blocks to external databases or content management systems. ```swift file=@cesdk_swift_examples/engine-guides-store-custom-metadata/StoreCustomMetadata.swift reference-only import Foundation import IMGLYEngine @MainActor func storeCustomMetadata(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(imageBlock, value: 400) try engine.block.setHeight(imageBlock, value: 300) try engine.block.setPositionX(imageBlock, value: 200) try engine.block.setPositionY(imageBlock, value: 150) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.appendChild(to: page, child: imageBlock) try engine.block.setMetadata(imageBlock, key: "externalId", value: "asset-12345") try engine.block.setMetadata(imageBlock, key: "source", value: "user-upload") try engine.block.setMetadata(imageBlock, key: "uploadedBy", value: "user@example.com") if try engine.block.hasMetadata(imageBlock, key: "externalId") { let externalId = try engine.block.getMetadata(imageBlock, key: "externalId") print("External ID:", externalId) } let allKeys = try engine.block.findAllMetadata(imageBlock) for key in allKeys { let value = try engine.block.getMetadata(imageBlock, key: key) print("\(key): \(value)") } struct GenerationInfo: Codable { let source: String let model: String let timestamp: Int64 } let info = GenerationInfo( source: "ai-generated", model: "stable-diffusion", timestamp: Int64(Date().timeIntervalSince1970 * 1000), ) let encoded = try JSONEncoder().encode(info) try engine.block.setMetadata( imageBlock, key: "generationInfo", value: String(data: encoded, encoding: .utf8)!, ) let raw = try engine.block.getMetadata(imageBlock, key: "generationInfo") let decoded = try JSONDecoder().decode(GenerationInfo.self, from: Data(raw.utf8)) print("Generated by \(decoded.model) at \(decoded.timestamp)") if try engine.block.hasMetadata(imageBlock, key: "uploadedBy") { try engine.block.removeMetadata(imageBlock, key: "uploadedBy") } let stillHasKey = try engine.block.hasMetadata(imageBlock, key: "uploadedBy") print("Has uploadedBy after removal:", stillHasKey) let remainingKeys = try engine.block.findAllMetadata(imageBlock) print("Remaining metadata keys:", remainingKeys) } ``` This guide covers how to set, retrieve, list, and remove metadata on blocks, as well as how to store structured data as JSON strings. ## Initialize CE.SDK We start with a fresh scene that contains a single image block. The metadata APIs live on `engine.block` and operate on any `DesignBlockID`, including the scene block itself. ```swift highlight-storeCustomMetadata-setup let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 800) try engine.block.setHeight(page, value: 600) try engine.block.appendChild(to: scene, child: page) let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(imageBlock, value: 400) try engine.block.setHeight(imageBlock, value: 300) try engine.block.setPositionX(imageBlock, value: 200) try engine.block.setPositionY(imageBlock, value: 150) let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.appendChild(to: page, child: imageBlock) ``` ## Set Metadata Use `engine.block.setMetadata(_:key:value:)` to attach a key-value pair to a block. Both the key and the value are `String`. If the key already exists, the value is overwritten. ```swift highlight-storeCustomMetadata-setMetadata try engine.block.setMetadata(imageBlock, key: "externalId", value: "asset-12345") try engine.block.setMetadata(imageBlock, key: "source", value: "user-upload") try engine.block.setMetadata(imageBlock, key: "uploadedBy", value: "user@example.com") ``` You can attach multiple metadata entries to a single block. Each entry is independent and can be accessed, modified, or removed separately. ## Get Metadata Use `engine.block.getMetadata(_:key:)` to retrieve a value by its key. The call throws if the key doesn't exist, so guard with `hasMetadata(_:key:)` for conditional access. ```swift highlight-storeCustomMetadata-getMetadata if try engine.block.hasMetadata(imageBlock, key: "externalId") { let externalId = try engine.block.getMetadata(imageBlock, key: "externalId") print("External ID:", externalId) } ``` `hasMetadata(_:key:)` returns `true` if the block has metadata for the given key, and `false` otherwise. This pattern keeps optional reads exception-free. ## List All Metadata Keys Use `engine.block.findAllMetadata(_:)` to get every metadata key stored on a block as a `[String]`. ```swift highlight-storeCustomMetadata-findAllMetadata let allKeys = try engine.block.findAllMetadata(imageBlock) for key in allKeys { let value = try engine.block.getMetadata(imageBlock, key: key) print("\(key): \(value)") } ``` This is useful for iterating through all metadata on a block or inspecting what is currently attached. ## Store Structured Data Metadata values must be strings, so encode richer data with `JSONEncoder` and decode it on read with `JSONDecoder`. Any `Codable` type — including a struct local to the function, as shown here — works. ```swift highlight-storeCustomMetadata-storeStructuredData struct GenerationInfo: Codable { let source: String let model: String let timestamp: Int64 } let info = GenerationInfo( source: "ai-generated", model: "stable-diffusion", timestamp: Int64(Date().timeIntervalSince1970 * 1000), ) let encoded = try JSONEncoder().encode(info) try engine.block.setMetadata( imageBlock, key: "generationInfo", value: String(data: encoded, encoding: .utf8)!, ) let raw = try engine.block.getMetadata(imageBlock, key: "generationInfo") let decoded = try JSONDecoder().decode(GenerationInfo.self, from: Data(raw.utf8)) print("Generated by \(decoded.model) at \(decoded.timestamp)") ``` This pattern lets you store generation parameters, configuration objects, or any other structured information that survives a scene save and load. ## Remove Metadata Use `engine.block.removeMetadata(_:key:)` to delete a key-value pair from a block. The call throws if the key doesn't exist, so guard with `hasMetadata(_:key:)` whenever the key may be absent. ```swift highlight-storeCustomMetadata-removeMetadata if try engine.block.hasMetadata(imageBlock, key: "uploadedBy") { try engine.block.removeMetadata(imageBlock, key: "uploadedBy") } ``` After removal, you can confirm the key is gone with `hasMetadata(_:key:)`. ```swift highlight-storeCustomMetadata-verifyRemoval let stillHasKey = try engine.block.hasMetadata(imageBlock, key: "uploadedBy") print("Has uploadedBy after removal:", stillHasKey) let remainingKeys = try engine.block.findAllMetadata(imageBlock) print("Remaining metadata keys:", remainingKeys) ``` ## Metadata Persistence Metadata is preserved when you save a scene with `engine.scene.saveToString()` or `engine.scene.saveToArchive(...)`. When you load that scene back with `engine.scene.load(from:)`, every metadata entry is restored on its block. > **Note:** Metadata only travels with scene data. Exporting a block to PNG, JPEG, PDF, or > MP4 produces a final asset that does not carry metadata, since the export > pipeline writes pixels and frames rather than scene structure. ## Troubleshooting ### getMetadata Throws If `getMetadata(_:key:)` throws, the key isn't set on the block. Always pair the call with `hasMetadata(_:key:)` as shown in the Get Metadata section above. ### removeMetadata Throws `removeMetadata(_:key:)` also throws when the key is missing. Guard with `hasMetadata(_:key:)` before calling it on data that may not be present, or wrap the call in `try?` if a missing key should be treated as a no-op. ### Metadata Lost After Load Confirm you're saving with `saveToString()` or `saveToArchive(...)` rather than exporting. Image, video, and PDF exports produce final assets and do not carry metadata. ### Large Metadata Values Metadata is designed for small strings. Very large values can slow down save and load operations. For large payloads, store a reference (a URL or external ID) instead of the data itself. ## API Reference | Method | Description | | ---------------------------------------- | ---------------------------------------- | | `engine.block.setMetadata(_:key:value:)` | Set a metadata key-value pair on a block | | `engine.block.getMetadata(_:key:)` | Get the value for a metadata key | | `engine.block.hasMetadata(_:key:)` | Check if a metadata key exists | | `engine.block.findAllMetadata(_:)` | List all metadata keys on a block | | `engine.block.removeMetadata(_:key:)` | Remove a metadata key-value pair | --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "Thumbnail Previews" description: "Stream video filmstrips, page storyboards, and audio waveforms in Swift with the CE.SDK thumbnail sequence APIs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/thumbnail-previews-dcf031/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Guides](https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/) > [Export Media Assets](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) > [Thumbnail Previews](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/thumbnail-previews-dcf031/) --- ```swift file=@cesdk_swift_examples/engine-guides-thumbnail-previews/ThumbnailPreviews.swift reference-only import CoreGraphics import Foundation import IMGLYEngine import SwiftUI #if canImport(UIKit) import UIKit #endif @MainActor func thumbnailPreviews(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let videoURL = baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ) let audioURL = baseURL.appendingPathComponent("ly.img.audio/audios/far_from_home.m4a") // Demo scaffolding: a ten-second video page carrying one video clip and one // audio clip, so every sequence below previews real media. In your app you // start from a scene the user is already editing. let scene = try engine.scene.createVideo() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1280) try engine.block.setHeight(page, value: 720) try engine.block.setDuration(page, duration: 10) let videoTrack = try engine.block.create(.track) try engine.block.appendChild(to: page, child: videoTrack) let clip = try engine.block.create(.graphic) try engine.block.setShape(clip, shape: engine.block.createShape(.rect)) try engine.block.setWidth(clip, value: 1280) try engine.block.setHeight(clip, value: 720) let videoFill = try engine.block.createFill(.video) try engine.block.setURL(videoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(clip, fill: videoFill) try engine.block.appendChild(to: videoTrack, child: clip) let audioTrack = try engine.block.create(.track) try engine.block.appendChild(to: page, child: audioTrack) let audioClip = try engine.block.create(.audio) try engine.block.setURL(audioClip, property: "audio/fileURI", value: audioURL) try engine.block.appendChild(to: audioTrack, child: audioClip) // Loading the media up front stops the first request from returning before // the resource is ready. try await engine.block.forceLoadAVResource(videoFill) try await engine.block.forceLoadAVResource(audioClip) try engine.block.setDuration(clip, duration: 8) try engine.block.setDuration(audioClip, duration: 10) let frameCount = 8 var filmstrip = [CGImage?](repeating: nil, count: frameCount) for try await frame in engine.block.generateVideoThumbnailSequence( videoFill, thumbnailHeight: 72, timeRange: 0 ... 8, numberOfFrames: frameCount, ) { // Frames arrive one at a time and carry their own position, so write each // one to the slot it reports instead of appending in arrival order. guard filmstrip.indices.contains(frame.frameIndex) else { continue } filmstrip[frame.frameIndex] = frame.image } guard let firstFrame = filmstrip[0] else { fatalError("Expected the filmstrip to start at frame 0.") } // `VideoThumbnail` carries no size of its own — read it off the `CGImage`. print("Frame 0 is \(firstFrame.width)x\(firstFrame.height) pixels") // SwiftUI renders a `CGImage` directly, on every Apple platform. let preview = Image(decorative: firstFrame, scale: 1) #if canImport(UIKit) // A UIKit view takes the same `CGImage` wrapped in a `UIImage`. let uiPreview = UIImage(cgImage: firstFrame) print("UIKit preview size: \(uiPreview.size)") #endif _ = preview var storyboard = [Int: CGImage]() for try await frame in engine.block.generateVideoThumbnailSequence( page, thumbnailHeight: 108, timeRange: 0 ... 10, numberOfFrames: 5, ) { storyboard[frame.frameIndex] = frame.image } print("Storyboard holds \(storyboard.count) composed frames") let samplesPerChunk = 64 let numberOfSamples = 200 let numberOfChannels = 2 var waveform = [Float]() var receivedChunks = 0 for try await chunk in engine.block.generateAudioThumbnailSequence( audioClip, samplesPerChunk: samplesPerChunk, timeRange: 0 ... 10, numberOfSamples: numberOfSamples, numberOfChannels: numberOfChannels, ) { receivedChunks += 1 // Stereo samples are interleaved, left channel first. Step by the channel // count to read one channel; the values are already a 0...1 envelope. for index in stride(from: 0, to: chunk.samples.count, by: numberOfChannels) { waveform.append(chunk.samples[index]) } } // The engine sends exactly this many chunks, and the last one may be short. let expectedChunks = Int(ceil(Double(numberOfSamples) / Double(samplesPerChunk))) print("Waveform: \(waveform.count) bars in \(receivedChunks) of \(expectedChunks) chunks") var posterFrame: CGImage? for try await frame in engine.block.generateVideoThumbnailSequence( page, thumbnailHeight: 256, timeRange: 2 ... 2, numberOfFrames: 1, ) { posterFrame = frame.image } print("Poster frame: \(posterFrame?.width ?? 0)x\(posterFrame?.height ?? 0)") // There is no cancel method. Leaving the loop drops the stream's iterator, // which cancels the request on the engine's next tick. var scrubbed = [CGImage]() for try await frame in engine.block.generateVideoThumbnailSequence( videoFill, thumbnailHeight: 72, timeRange: 0 ... 8, numberOfFrames: 64, ) { scrubbed.append(frame.image) if scrubbed.count == 4 { break } } // In a view, own the iteration in a `Task` — SwiftUI's `.task(id:)` does this // for you — so replacing or dismissing the view cancels the request it made. let storyboardTask = Task { @MainActor in var received = 0 for try await _ in engine.block.generateVideoThumbnailSequence( page, thumbnailHeight: 108, timeRange: 0 ... 10, numberOfFrames: 40, ) { try Task.checkCancellation() received += 1 } return received } storyboardTask.cancel() let deliveredBeforeCancel = (try? await storyboardTask.value) ?? 0 print("Stopped after \(scrubbed.count) frames; cancelled task saw \(deliveredBeforeCancel)") } ``` Build timeline filmstrips, page storyboards, and waveform lanes in Swift by streaming lightweight previews out of CE.SDK as each frame or sample chunk becomes ready. > **Reading time:** 9 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v1.86.2/engine-guides-thumbnail-previews) > **Note:** This guide assumes you already have an `Engine` reference. Learn more in the > [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide. CE.SDK exposes two preview APIs on `BlockAPI`. `generateVideoThumbnailSequence(_:thumbnailHeight:timeRange:numberOfFrames:)` streams image frames sampled across a time range, and `generateAudioThumbnailSequence(_:samplesPerChunk:timeRange:numberOfSamples:numberOfChannels:)` streams a waveform as chunks of `Float` samples. Both return an `AsyncThrowingStream` you consume with `for try await`. Both are built for previews, so they render at most 512 px per side. Ask for a taller thumbnail and you still get the size you asked for, but the extra pixels are stretched, not sharper. For full-resolution still images, see [Create Thumbnail](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/create-thumbnail-749be1/) instead. ## Generate a Video Filmstrip Pass a video fill, the pixel height you want, the time range, and how many frames to sample. The engine derives each frame's width from the video's aspect ratio, so you only ever specify height. ```swift highlight-tpios-filmstrip let frameCount = 8 var filmstrip = [CGImage?](repeating: nil, count: frameCount) for try await frame in engine.block.generateVideoThumbnailSequence( videoFill, thumbnailHeight: 72, timeRange: 0 ... 8, numberOfFrames: frameCount, ) { // Frames arrive one at a time and carry their own position, so write each // one to the slot it reports instead of appending in arrival order. guard filmstrip.indices.contains(frame.frameIndex) else { continue } filmstrip[frame.frameIndex] = frame.image } ``` Because the stream is created eagerly, the engine begins working the moment you call the method — before you start iterating. Neither method is `async` or `throws`; errors surface when you consume the stream. ### Choosing the Block: Video Fill or Design Block The same method behaves differently depending on what you pass, and picking the wrong one is the most common source of surprising frame times. | | Video fill | Page or design block | | --- | --- | --- | | What it renders | Decoded frames from the media file | The composed scene, re-rendered | | Frame times | Evenly spaced, including the first and last moment of the range | Spread across the range, but never exactly the first or last moment | | Time base | Media time in the source file — trim offset, trim length, time offset, speed, and looping are all ignored | Relative to the block's start on the page timeline | | Speed | Slower, because each frame is decoded on its own | Faster, because several frames render together | | Animations | Not applicable | Suppressed; blocks render un-animated | For a page or design block, the range starts when the block starts on the page. For a block inside a group or a track, that is its own time offset plus the time offsets of the groups and tracks around it. With `numberOfFrames: 1`, a video fill returns the frame at `timeRange.lowerBound`. A page or design block never lands exactly on the ends of the range, so one frame over `0 ... 10` gives you the frame at 5 seconds. Pass a page to build a storyboard of the composition over time: ```swift highlight-tpios-storyboard var storyboard = [Int: CGImage]() for try await frame in engine.block.generateVideoThumbnailSequence( page, thumbnailHeight: 108, timeRange: 0 ... 10, numberOfFrames: 5, ) { storyboard[frame.frameIndex] = frame.image } print("Storyboard holds \(storyboard.count) composed frames") ``` The block must be a page or sit on one. A block you have not added to a page yet, or the scene itself, fails with `MEDIA.BLOCK_NOT_PAGE_OR_CHILD`. Video inside the composition comes from a small set of cached frames rather than a fresh decode, so a storyboard shows video content only approximately. When you need accurate frames from a clip, ask its video fill instead. Video fills and design blocks — pages, graphics, text, groups, and tracks — are accepted. Every other kind of block is rejected with `MEDIA.OPERATION_UNSUPPORTED_FOR_BLOCK`: image fills, color fills, gradient fills, shapes, effects, and animations. ### Handling Frames as They Stream In Each `VideoThumbnail` carries a `frameIndex` and a `CGImage`. Frames are not guaranteed to arrive in index order, so key off the reported index rather than appending in arrival order. There is no completion callback in the engine. The Swift stream finishes for you once the final index arrives, so `for try await` ends on its own — but do not write your own completion test against `frameIndex == numberOfFrames - 1`. When a request fails, the reported index is always `0`, whichever frame actually failed. Count arrivals instead. An error is terminal for the whole sequence: no further frames arrive after one, and the stream throws out of your loop. `VideoThumbnail` has no width or height properties. Read the dimensions from the `CGImage`, and hand that same image to whichever view layer you render with. ```swift highlight-tpios-image guard let firstFrame = filmstrip[0] else { fatalError("Expected the filmstrip to start at frame 0.") } // `VideoThumbnail` carries no size of its own — read it off the `CGImage`. print("Frame 0 is \(firstFrame.width)x\(firstFrame.height) pixels") // SwiftUI renders a `CGImage` directly, on every Apple platform. let preview = Image(decorative: firstFrame, scale: 1) #if canImport(UIKit) // A UIKit view takes the same `CGImage` wrapped in a `UIImage`. let uiPreview = UIImage(cgImage: firstFrame) print("UIKit preview size: \(uiPreview.size)") #endif ``` ## Draw an Audio Waveform Pass an audio block or a video fill. `numberOfSamples` is the number of samples **per channel**, and `samplesPerChunk` controls how many arrive at a time. The stream yields `numberOfSamples / samplesPerChunk` chunks, rounded up, and the last one can be shorter than the rest. ```swift highlight-tpios-waveform let samplesPerChunk = 64 let numberOfSamples = 200 let numberOfChannels = 2 var waveform = [Float]() var receivedChunks = 0 for try await chunk in engine.block.generateAudioThumbnailSequence( audioClip, samplesPerChunk: samplesPerChunk, timeRange: 0 ... 10, numberOfSamples: numberOfSamples, numberOfChannels: numberOfChannels, ) { receivedChunks += 1 // Stereo samples are interleaved, left channel first. Step by the channel // count to read one channel; the values are already a 0...1 envelope. for index in stride(from: 0, to: chunk.samples.count, by: numberOfChannels) { waveform.append(chunk.samples[index]) } } // The engine sends exactly this many chunks, and the last one may be short. let expectedChunks = Int(ceil(Double(numberOfSamples) / Double(samplesPerChunk))) print("Waveform: \(waveform.count) bars in \(receivedChunks) of \(expectedChunks) chunks") ``` Each chunk carries `samplesPerChunk * numberOfChannels` values, interleaved left channel first for stereo. Step the array by `numberOfChannels` to read a single channel. Only audio blocks and video fills produce waveforms. Passing a page or a graphic block is rejected, unlike the video sequence API, which accepts both. ### What the Numbers Mean The engine analyzes the audio for you. Every value is a loudness between `0` and `1`, never negative, and already smoothed — so these are numbers you can draw straight away, not raw audio data. Draw them by mirroring each value around a center line. You do not need to find peaks or rescale anything, and very quiet audio reads as silence. Requesting a range past the end of the media is not an error — you get zero-filled chunks. `numberOfChannels` must be `1` or `2`. ## Capture a Single Preview Frame A one-frame request is the quickest way to get a single preview image. Pass the same time as both bounds of the range and ask for one frame. ```swift highlight-tpios-single-frame var posterFrame: CGImage? for try await frame in engine.block.generateVideoThumbnailSequence( page, thumbnailHeight: 256, timeRange: 2 ... 2, numberOfFrames: 1, ) { posterFrame = frame.image } print("Poster frame: \(posterFrame?.width ?? 0)x\(posterFrame?.height ?? 0)") ``` Keep `thumbnailHeight` at 512 or below. Above that, the engine upscales from a 512 px render, so you pay for the larger buffer without gaining detail. ## Cancel Generation There is no cancel method on iOS or macOS. The stream is the handle: when its iterator goes away, the engine request is cancelled. That happens when you `break` out of the loop, when the `Task` that iterates is cancelled, or when a SwiftUI `.task` or `.task(id:)` modifier tears down. ```swift highlight-tpios-cancel // There is no cancel method. Leaving the loop drops the stream's iterator, // which cancels the request on the engine's next tick. var scrubbed = [CGImage]() for try await frame in engine.block.generateVideoThumbnailSequence( videoFill, thumbnailHeight: 72, timeRange: 0 ... 8, numberOfFrames: 64, ) { scrubbed.append(frame.image) if scrubbed.count == 4 { break } } // In a view, own the iteration in a `Task` — SwiftUI's `.task(id:)` does this // for you — so replacing or dismissing the view cancels the request it made. let storyboardTask = Task { @MainActor in var received = 0 for try await _ in engine.block.generateVideoThumbnailSequence( page, thumbnailHeight: 108, timeRange: 0 ... 10, numberOfFrames: 40, ) { try Task.checkCancellation() received += 1 } return received } storyboardTask.cancel() let deliveredBeforeCancel = (try? await storyboardTask.value) ?? 0 print("Stopped after \(scrubbed.count) frames; cancelled task saw \(deliveredBeforeCancel)") ``` A cancel takes effect a moment later, so one or two frames can still arrive. Nothing arrives to tell you the cancel happened — the frames simply stop. Cancelling twice or cancelling a request that already finished is safe. How much a cancel actually stops depends on which sequence you started: | Sequence | Cancel before the first element | Cancel mid-flight | | --- | --- | --- | | Video fill | Stops it — nothing is yielded | **No effect.** The remaining frames are still produced | | Page or design block | Stops it | Stops it | | Audio waveform | Stops it | Stops it | Plan around the first row. Once the engine starts decoding a video fill, a cancel no longer reaches it. Ending the stream does stop your loop from seeing those frames, so your UI stays correct — but the engine keeps working, and that work delays your next request. Keep video fill strips short, eight frames rather than a hundred, so an abandoned request finishes quickly. Always cancel before you request the same block again. A block handles one request at a time, and a second request waits instead of failing. Reusing a block without cancelling is what makes a scrubber feel stuck. ## Performance and Limits - **512 px render cap.** `thumbnailHeight` above 512 produces a larger image upscaled from a 512 px render. - **One request per block at a time.** A second request waits instead of failing. - **Requests run one after another.** Several strips started at once wait for each other. - **Video fills are slower than pages.** Each frame from a video file is decoded on its own, so a 100-frame filmstrip takes noticeably longer than a page storyboard of the same length. - **Compressed audio takes longer.** A WAV file produces its waveform almost immediately; a long MP3 or AAC file arrives over several moments. - Both methods are declared on `@MainActor` types — call them from the main actor. `VideoThumbnail` and `AudioThumbnail` are `Sendable`, so you can iterate anywhere. - Calling `forceLoadAVResource(_:)` first is optional; both methods wait for the media on their own. It downloads the media before you ask for previews, so the first request does not spend that time waiting. ## Troubleshooting - **The sequence never seems to complete.** You are testing `frameIndex == numberOfFrames - 1` as a completion signal. The reported index is always `0` on error, so that test can never fire. Count arrivals instead, and let the stream's own termination end the loop. - **`MEDIA.OPERATION_UNSUPPORTED_FOR_BLOCK`.** You passed a render block that is not a video fill — an image fill, a shape, or an effect. - **`MEDIA.BLOCK_NOT_PAGE_OR_CHILD`.** The design block is detached from the scene tree, or it is the scene block itself. - **The waveform is flat.** The time range is past the end of the audio, or the audio is very quiet. - **Thumbnails look soft above 512 px.** That is the render cap, not a decode problem. Request 512 or less. - **A scrubber stops updating after a fast drag.** Each new request is waiting behind the previous one. Cancel the running request before you start the next. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.generateVideoThumbnailSequence(_:thumbnailHeight:timeRange:numberOfFrames:)` | Streams `numberOfFrames` image frames across `timeRange` from a video fill or design block. Returns `AsyncThrowingStream`. | | `engine.block.generateAudioThumbnailSequence(_:samplesPerChunk:timeRange:numberOfSamples:numberOfChannels:)` | Streams a waveform from an audio block or video fill. Returns `AsyncThrowingStream`. | | `engine.block.forceLoadAVResource(_:)` | Loads a video fill or audio block before requesting previews from it. | The exact signatures: ```swift public func generateVideoThumbnailSequence( _ id: DesignBlockID, thumbnailHeight: Int, timeRange: ClosedRange, numberOfFrames: Int, ) -> AsyncThrowingStream public func generateAudioThumbnailSequence( _ id: DesignBlockID, samplesPerChunk: Int, timeRange: ClosedRange, numberOfSamples: Int, numberOfChannels: Int, ) -> AsyncThrowingStream ``` ### Properties | Property | Type | Description | | --- | --- | --- | | `VideoThumbnail.frameIndex` | `Int` | Position of this frame in the requested sequence. Always `0` when the request failed. | | `VideoThumbnail.image` | `CGImage` | The rendered frame. Read `image.width` and `image.height` for its size. | | `AudioThumbnail.chunkIndex` | `Int` | Position of this chunk in the requested sequence. | | `AudioThumbnail.samples` | `[Float]` | Channel-interleaved loudness values in `0...1`. The final chunk may be shorter than `samplesPerChunk * numberOfChannels`. | ## Next Steps - [Create Thumbnail](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/create-thumbnail-749be1/) — Export a page to a static preview image with target dimensions. - [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) — Arrange tracks and clips for the timeline surface these previews decorate. - [Trim](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) — Set trim offsets and lengths, which the video-fill preview path deliberately ignores. --- ## More Resources - **[Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md)** - Browse all Mac Catalyst documentation - **[Complete Documentation](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt)** - Full documentation in one file (for LLMs) - **[Web Documentation](https://img.ly/docs/cesdk/mac-catalyst/)** - Interactive documentation with examples - **[Support](mailto:support@img.ly)** - Contact IMG.LY support --- --- title: "File Format Support" description: "See which image, video, audio, font, and template formats CE.SDK supports for import and export." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/file-format-support-3c4b2a/" --- > This is one page of the CE.SDK Mac Catalyst documentation. For a complete overview, see the [Mac Catalyst Documentation Index](https://img.ly/docs/cesdk/mac-catalyst.md). For all docs in one file, see [llms-full.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-full.txt). **Navigation:** [Compatibility & Security](https://img.ly/docs/cesdk/mac-catalyst/compatibility-fef719/) > [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/file-format-support-3c4b2a/) --- CreativeEditor SDK (CE.SDK) supports a wide range of modern file types for importing assets and exporting final content. Whether you're working with images, videos, audio, documents, or fonts, CE.SDK provides a client-side editing environment with excellent media compatibility and performance—optimized for modern client-side hardware. This guide outlines supported formats, codecs, and known limitations across media types. ## Importing Media | Category | Supported Formats | | ------------- | ------------------------------------------------------------------ | | **Images** | `.png`, `.apng`, `.jpeg`, `.jpg`, `.gif`, `.webp`, `.svg`, `.bmp` | | **Video** | `.mp4` (H.264/AVC, H.265/HEVC), `.mov` (H.264/AVC, H.265/HEVC), `.webm` (VP8, VP9, AV1) | | **Audio** | `.wav`, `.mp3`, `.m4a`, `.mp4` (AAC or MP3), `.mov` (AAC or MP3), `.webm` (Opus or Vorbis) | | **Animation** | `.json` (Lottie), `.lottie` (dotLottie) | Animated images (`.gif` and `.apng`) import as a static first frame in design scenes and as a looping video fill in video scenes. A Lottie `.json` file or a dotLottie `.lottie` archive set as a video fill plays as one block. > **Note:** Need to import a format not listed here? CE.SDK allows you to create custom > importers for any file type by using our Scene and Block APIs > programmatically. ### SVG Limitations CE.SDK uses Skia for SVG parsing and rendering. While most SVG files render correctly, there are some important limitations to be aware of: ### Text Elements - SVG text elements are not supported - any text in SVG files will not be rendered. - Convert text to paths in your vector editor before exporting if text is needed. ### Styling Limitations - CSS rules in `