--- 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. --- ## 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/v$UBQ_VERSION$/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 uses radians where `0` is right, `0.5 * .pi` is bottom, `.pi` is left, and `1.5 * .pi` is top — the snippet above slides the block in from the top. ## 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**: Set in radians for slide animations (`0` = right, `0.5 * .pi` = bottom, `.pi` = left, `1.5 * .pi` = top) - **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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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: "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/) ## 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. CE.SDK animations are preset effects, so they do not expose custom per-property keyframe editing. 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. - [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: "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/v$UBQ_VERSION$/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 in from a specified direction. The `animation/slide/direction` property uses radians where `0` is right, `.pi / 2` is bottom, `.pi` is left, and `3 * .pi / 2` is top. ```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 final dimensions. 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 ## 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 , the following packages are available: - [IMGLYEngine](`$\{props.platform.slug}/api-reference/documentation/imglyengine/`) {props.platform.id === 'ios' && ( <> )} --- ## 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/v$UBQ_VERSION$/engine-guides-auto-resize) 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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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 are scene-scoped and persist for the lifetime of the engine session, 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/v$UBQ_VERSION$/engine-guides-design-generation) 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 and merges the template's variables into the engine, replacing matching keys without clearing unrelated variables. `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()` is engine-wide and can still contain variables from a previously loaded scene. ```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 engine-wide variable keys; this is not a per-template inventory. | | `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/$UBQ_VERSION$/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/) ### Output Formats ## 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/v$UBQ_VERSION$/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 . --- ## 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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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") 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 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 controls opacity) 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") } } } ``` 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/v$UBQ_VERSION$/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. ## 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 controls opacity. ```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 controls opacity) 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) ``` When rendered on screen, CMYK colors are converted to RGB using standard conversion formulas. The `tint` value (0.0 to 1.0) is rendered as transparency. > **Note:** During PDF export, CMYK colors are currently converted to RGB using the standard conversion. Tint values are retained in the alpha channel. ## 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` controls opacity when rendered on screen. ```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 or CMYK approximation. 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") } } ``` ## Choosing the Right Color Space | Color Space | Use Case | Output | |-------------|----------|--------| | **sRGB** | Web, digital, screen display | PNG, JPEG, WebP | | **CMYK** | Print workflows (converts to RGB) | PDF (converted) | | **Spot Color** | Specialized printing, brand colors | PDF (Separation Color Space) | ## 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. | | 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 controls opacity. | | `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 --- ## 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) 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/v$UBQ_VERSION$/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 values are preserved during 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. ## 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) 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 tint value from CMYK or spot colors becomes the alpha value in the returned sRGB color. ## 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)") ``` When converting sRGB colors to CMYK, the alpha value becomes the tint value of the returned CMYK color. For spot colors, define a CMYK approximation with `setSpotColor(name:c:m:y:k:)` before converting. > **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 The tint and alpha values represent transparency in different color spaces: | Source | Target | Transformation | |--------|--------|----------------| | sRGB (alpha) | CMYK | Alpha becomes tint | | CMYK (tint) | sRGB | Tint becomes alpha | | Spot (tint) | sRGB | Tint becomes alpha | | Spot (tint) | CMYK | Tint is preserved | ## 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 opacity. | | `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. `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. 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/v$UBQ_VERSION$/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. 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), ], ) ``` ## 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. | | 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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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 preserve the wider gamut by writing 16-bit Display P3 PNGs with an embedded ICC profile. 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. ## 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 | 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/v$UBQ_VERSION$/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)") } 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 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/) --- ## 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/v$UBQ_VERSION$/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/) --- ## 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. - [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. - [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. - [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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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) // 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) // 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/v$UBQ_VERSION$/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`, 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) ``` Using the wrong accessor type for a property causes an error. Always check `getType(ofProperty:)` if you're unsure which accessor to use. ## 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(_:)`**: Operation failed (`audioDecoding`, `imageDecoding`, `fileFetch`, `videoDecoding`, `unknown`) ```swift highlight-block-state // Query block state - indicates loading status let graphicState = try engine.block.getState(graphic) print("Graphic state:", graphicState) // 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/v$UBQ_VERSION$/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 in memory until you explicitly destroy them with `engine.editor.destroyBuffer()` or the engine instance is disposed. For large buffers or long editing sessions, destroy buffers when they're no longer needed to free memory. ## 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: "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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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.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_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/) | ## 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_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_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_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_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_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.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.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.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.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_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_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_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.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_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_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_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.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_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_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_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 numeric value. Strings, NaN, and infinities are rejected. | [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) | | `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.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_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_INVALID_RESOLUTION` | Invalid resolution for video encoder: \{width} x \{height} | Resolution must be > 0 in both dimensions. 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. | | ## 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.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.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.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_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.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.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.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. 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.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_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_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, e.g. \{"sampleRate": 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" or "audio/mp4". | Pass audio/wav or audio/mp4 as the target mime type for audio export. | | | `ENCODE.AUDIO_MP4_NO_DATA_MUXED` | No audio data was muxed into MP4 container. | The muxer received no PCM frames. Check the source produced audio packets before finalization. | | | `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_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.BLOCK_MUST_BE_PAGE` | The block to export must be a page. | Audio export targets a page block. Pass a page or call api.scene.getPages() to obtain one. | | | `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.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_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, e.g. \{"pngCompressionLevel": 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.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. | Some scene resources are still pending. Subscribe to resource events or poll until all assets reach Ready, then retry. | | | `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_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` | The effective rendering size of \{width}x\{height} px (at \{dpi} DPI) for the PDF export exceeds the maximum supported texture size \{maxSize} of the device. Try reducing the scene DPI or the size of the exported block. | Reduce the scene DPI or the export size so the rendered dimensions stay at or below \{maxSize} px. | | | `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.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/PDF 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_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_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_EXPORT_OPTIONS_NOT_OBJECT` | Video export options must be a valid JSON object. | Pass the video export options as a JSON object string, e.g. \{"videoBitrate": 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_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". | Video export currently only supports video/mp4 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. | | ## 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.CANNOT_DEACTIVATE_OFFLINE` | Cannot deactivate offline license. | Offline licenses cannot be deactivated remotely. Switch to an online license if dynamic activation is required. | | | `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.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.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_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_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. Confirm it was produced by api.block.saveToString() or saveToArchive(). | [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} | The scene references resources via blocked URL schemes (\{schemes}). Update the policy or rewrite the scene's resource URLs. | [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.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.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.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.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, or AMR (samr/sawb) 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_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.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_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.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/v$UBQ_VERSION$/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: "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/v$UBQ_VERSION$/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. ## 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.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: "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) } ``` 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/v$UBQ_VERSION$/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`. 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. ### 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 | ## 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/v$UBQ_VERSION$/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)") 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/v$UBQ_VERSION$/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. ## 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/v$UBQ_VERSION$/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/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/v$UBQ_VERSION$/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 - 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: When it comes to exporting or converting designs, the SDK supports the following output formats: Each format serves different use cases, giving you the flexibility to adapt designs for your application's needs. --- ## 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/v$UBQ_VERSION$/engine-guides-conversion-to-base64) 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/v$UBQ_VERSION$/engine-guides-to-blob) 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/v$UBQ_VERSION$/engine-guides-conversion-to-pdf) 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 and gradients remain vector-drawn where possible. | | 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`. | | `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: "To PNG" description: "Export CE.SDK scenes and pages to PNG with configurable compression, target dimensions, and text overhang handling." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/conversion/to-png-f1660c/" --- > 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). --- ```swift file=@cesdk_swift_examples/engine-guides-conversion-to-png/ConversionToPng.swift reference-only import Foundation import IMGLYEngine @MainActor func conversionToPng(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.getCurrentPage()! let pngData = try await engine.block.export(page, mimeType: .png) let pages = try engine.scene.getPages() var exportedPages: [Data] = [] for try await data in try await engine.block.export(pages, mimeType: .png) { exportedPages.append(data) } let compressedOptions = ExportOptions(pngCompressionLevel: 9) let compressedData = try await engine.block.export(page, mimeType: .png, options: compressedOptions) let resizedOptions = ExportOptions(targetWidth: 1920, targetHeight: 1080) let resizedData = try await engine.block.export(page, mimeType: .png, options: resizedOptions) let overhangOptions = ExportOptions(allowTextOverhang: true) let overhangData = try await engine.block.export(page, mimeType: .png, options: overhangOptions) } ``` Export designs to PNG format with lossless quality and optional transparency support. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-conversion-to-png) PNG is a lossless image format that preserves image quality and supports transparency. It's ideal for designs requiring pixel-perfect fidelity, logos, graphics with transparent backgrounds, and any content where quality cannot be compromised. This guide covers how to export designs to PNG and configure export options using the Engine API. ## Export to PNG Use `engine.block.export(_:mimeType:)` to export a design block to PNG. The method returns `Data` containing the image. ```swift highlight-conversionToPng-exportSinglePage let page = try engine.scene.getCurrentPage()! let pngData = try await engine.block.export(page, mimeType: .png) ``` ## Export All Pages Export all pages in a scene 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-conversionToPng-exportAllPages let pages = try engine.scene.getPages() var exportedPages: [Data] = [] for try await data in try await engine.block.export(pages, mimeType: .png) { exportedPages.append(data) } ``` The batch API reuses a single worker engine for all exports, making it more memory efficient than exporting pages individually. ## Compression Level Control the file size versus export speed tradeoff using `pngCompressionLevel` in `ExportOptions`. Valid values are 0-9, where higher values produce smaller files but take longer to export. Since PNG is lossless, image quality remains unchanged. ```swift highlight-conversionToPng-compressionLevel let compressedOptions = ExportOptions(pngCompressionLevel: 9) let compressedData = try await engine.block.export(page, mimeType: .png, options: compressedOptions) ``` The default compression level is 5, providing a good balance between file size and export speed. ## Target Dimensions Resize the output by setting `targetWidth` and `targetHeight`. The block scales to fill the target dimensions while maintaining its aspect ratio. ```swift highlight-conversionToPng-targetDimensions let resizedOptions = ExportOptions(targetWidth: 1920, targetHeight: 1080) let resizedData = try await engine.block.export(page, mimeType: .png, options: resizedOptions) ``` Both values must be set together. A value of `0` (the default) uses the block's native size. ## Text Overhang Decorative fonts sometimes have glyphs that extend beyond their frame. Set `allowTextOverhang` to `true` to prevent clipping these glyphs during export. ```swift highlight-conversionToPng-textOverhang let overhangOptions = ExportOptions(allowTextOverhang: true) let overhangData = try await engine.block.export(page, mimeType: .png, options: overhangOptions) ``` ## API Reference | API | Description | | --- | --- | | `engine.block.export(_:mimeType:options:)` | Exports a single block to `Data` with the specified options | | `engine.block.export(_:mimeType:options:)` (batch) | Exports multiple blocks, returning an `AsyncThrowingStream` of `Data` | | `engine.scene.getCurrentPage()` | Returns the current page block ID | | `engine.scene.getPages()` | Returns all page block IDs in the scene | | `ExportOptions(pngCompressionLevel:targetWidth:targetHeight:allowTextOverhang:)` | Configures PNG export options | ## Next Steps - [Conversion Overview](https://img.ly/docs/cesdk/mac-catalyst/conversion/overview-44dc58/) - Learn about other export formats - [To PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) - Export designs to PDF format - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) - Understand the full export 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/v$UBQ_VERSION$/engine-guides-create-audio-audio) 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. `AudioFromVideoOptions(keepTrimSettings: true, muteOriginalVideo: true)` mirrors the source video's trim onto the extracted block and silences the original fill so the audio plays from the extracted block alone. ```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) ``` 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` returns the audio data without running the encoder; set it to `false` (the default) to receive a fully encoded WAV file. Pass `MIMEType.mp4` instead of `.wav` for an MP4 container. ```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:)` | Configure trim mirroring and source muting | | 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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/engine-guides-create-audio-audio-fade) 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. ## 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/v$UBQ_VERSION$/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. - [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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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. ## 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/v$UBQ_VERSION$/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. > **Note:** Groups are not currently available when editing videos. 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 > * Blocks already part of a group cannot be grouped again until ungrouped ## 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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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. ## 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/v$UBQ_VERSION$/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: "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/v$UBQ_VERSION$/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. ### 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/) --- ## Exporting Compositions CE.SDK compositions can be exported in several 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: "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/v$UBQ_VERSION$/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 ### 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 ## 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/v$UBQ_VERSION$/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: $SWIFT_VERSION$ // swift-tools-version: $SWIFT_VERSION$ 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: "$UBQ_VERSION$"), // 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/v$UBQ_VERSION$/engine-guides-dynamic-content) 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. - [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/v$UBQ_VERSION$/engine-guides-create-templates-form-based-editing) 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/v$UBQ_VERSION$/engine-guides-placeholders) 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/v$UBQ_VERSION$/engine-guides-set-editing-constraints) 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/v$UBQ_VERSION$/engine-guides-text-variables) 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. 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 stored on the engine. 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 currently stored on the engine. 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 variable keys currently stored on the engine | | `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: "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/v$UBQ_VERSION$/engine-guides-create-templates-add-to-template-library) 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/v$UBQ_VERSION$/engine-guides-edit-or-remove-templates) 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/v$UBQ_VERSION$/engine-guides-create-templates-from-scratch) 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 runtime values. `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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/engine-guides-import-from-scene-file) 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/v$UBQ_VERSION$/engine-guides-lock-template) 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/) These imported designs can then be adapted into editable, structured templates inside 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: "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. - [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/v$UBQ_VERSION$/engine-guides-apply-transitions) 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. ## 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` - **None**: `.none` ## 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/v$UBQ_VERSION$/engine-guides-create-video-control) 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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/engine-guides-lock-video-design) 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: "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/v$UBQ_VERSION$/engine-guides-create-video-programmatic) 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/v$UBQ_VERSION$/engine-guides-timeline-editor) 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/v$UBQ_VERSION$/engine-guides-update-caption-presets) 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, 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. ## 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/v$UBQ_VERSION$/engine-guides-edit-image-add-watermark) 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/v$UBQ_VERSION$/engine-guides-annotation) 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/v$UBQ_VERSION$/engine-guides-colors-replace) 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/$UBQ_VERSION$/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/v$UBQ_VERSION$/engine-guides-edit-image-transform-move) 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/v$UBQ_VERSION$/engine-guides-edit-image-transform-resize) 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.setSettingBool("touch/rotateAction", value: false) ``` 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/v$UBQ_VERSION$/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.setSettingBool("touch/pinchAction", value: true) ``` Setting this to false disables pinch scaling entirely. 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 true (or 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, drop shadow, and background with dedicated styling // setters, then use property-keyed setters for 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.setBackgroundColor(caption1, 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/v$UBQ_VERSION$/engine-guides-captions) 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, drop shadow, and background with dedicated styling setters, then uses property-keyed setters for automatic font sizing. ```swift highlight-addCaptions-styleCaptions // Style properties also sync only with caption blocks under the same caption // track. Set text color, drop shadow, and background with dedicated styling // setters, then use property-keyed setters for 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.setBackgroundColor(caption1, 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. ## 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.setBackgroundColor(_:r:g:b:a:)` | Set caption background 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/v$UBQ_VERSION$/engine-guides-create-video-add-watermark) 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/v$UBQ_VERSION$/engine-guides-annotation) ## 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/v$UBQ_VERSION$/engine-guides-join-and-arrange-video) 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/v$UBQ_VERSION$/engine-guides-create-video-edit-programmatic) 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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/engine-guides-video-transform) 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. `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. | | `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/v$UBQ_VERSION$/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.setSettingBool("touch/rotateAction", value: false) ``` 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.setSettingBool("touch/pinchAction", value: false) ``` 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/v$UBQ_VERSION$/engine-guides-create-video-trim) 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/v$UBQ_VERSION$/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: "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 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. ## 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. - **Audio-only exports.** Exporting audio doesn't currently record an export event. ## When an Export Is Counted The moment an export is recorded differs between still and video exports: - **Images and PDFs** are counted after the export completes successfully. A failed export—for example due to an invalid block or an encoding error—isn't counted. - **Videos** are counted when encoding starts. A video export that fails or is canceled during encoding still counts. 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. ## 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 export events include the media type, format, resolution and page count. Video export events include the media type, format, resolution, frame rate and duration. 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. 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 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/v$UBQ_VERSION$/engine-guides-create-thumbnail) 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. It does not cover audio waveforms or multi-frame scrubber previews. ## 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") ``` ## 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.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. - [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. - [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 | | 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 aren’t compressed by default. Use `exportPdfWithHighCompatibility` when you need broad software support in print workflows. > **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/v$UBQ_VERSION$/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) 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 audioOptions = AudioExportOptions(skipEncoding: true) let audioStream = try await engine.block.exportAudio(audioBlock, mimeType: .wav, options: audioOptions) 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 | | PDF | `application/pdf` | Partial | Print, documents | | MP4 | `video/mp4` | No | Animated content | | 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. ## 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 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. 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 and effects (like gradients) according to the scene's DPI setting for broader viewer support. | | `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` | `30` | Target framerate in Hz. | | `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. The example below sets `skipEncoding: true` to return the raw PCM buffer directly; set it to `false` (the default) to receive a fully encoded WAV file. ```swift highlight-exportOverview-audio let audioOptions = AudioExportOptions(skipEncoding: true) let audioStream = try await engine.block.exportAudio(audioBlock, mimeType: .wav, options: audioOptions) 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` | Return raw audio data without encoding to the target MIME type. | 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. `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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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 ## 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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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")) } } 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. ## 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. The default framerate is 30 Hz. ### 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` | `30` | Target framerate in Hz. | | `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. | ## API Reference | Method | Description | | ----------------------------------------------- | -------------------------------------------------------------------------------------------- | | `engine.block.exportVideo(_:mimeType:options:)` | Export a page block as MP4 video. Returns an `AsyncThrowingStream`. | ## Next Steps - [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 --- ## 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 @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")) 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/v$UBQ_VERSION$/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, 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. ## 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. ## 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. ## 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`. | | `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. Must be used with `targetHeight`. | | `targetHeight` | Target output height in pixels. Must be used with `targetWidth`. | ## API Reference | Method | Description | | ------ | ----------- | | `engine.block.export(_:mimeType:options:)` | Export a block as PDF with format and compatibility options | | `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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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 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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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. ## 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")) ``` ## 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 and gradients at scene DPI (default: `true`). | | `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/v$UBQ_VERSION$/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 `30`) | | `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/v$UBQ_VERSION$/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/v$UBQ_VERSION$/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() let archiveBlob = try await engine.scene.saveToArchive() let compressed = try await engine.scene.saveToString( options: SaveToStringOptions( compression: CompressionOptions(format: .zstd, level: .default), ), ) _ = compressed 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. ## 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. ## 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. ## Compression Options CE.SDK supports optional compression for saved scenes to reduce file size. Compression is particularly useful for large scenes or when storage space is limited. ```swift highlight-saveDesigns-compression let compressed = try await engine.scene.saveToString( options: SaveToStringOptions( compression: CompressionOptions(format: .zstd, level: .default), ), ) ``` **Compression Formats:** - `CompressionFormat.none` — No compression (default) - `CompressionFormat.zstd` — Zstandard compression (recommended for best performance) **Compression Levels:** - `CompressionLevel.fastest` — Fastest compression, larger output - `CompressionLevel.default` — Balanced speed and size (recommended) - `CompressionLevel.best` — Best compression, slower Compression adds minimal overhead while reducing scene size by approximately 64%. 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/v$UBQ_VERSION$/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: "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/) --- ## Importing Media ### SVG Limitations ## Exporting Media ## Importing Templates ## Font Formats ## Video & Audio Codecs CE.SDK supports the most widely adopted video and audio codecs to ensure compatibility across platforms: ## Size Limits ### Image Resolution Limits ### Video Resolution & Duration Limits --- ## 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: "Fills" description: "Apply solid colors, gradients, images, or videos as fills to shapes, text, and other design elements." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/fills-402ddc/" --- > 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/) > [Fills](https://img.ly/docs/cesdk/mac-catalyst/fills-402ddc/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/) - Apply solid colors, gradients, images, or videos as fills to shapes, text, and other design elements. - [Color Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/color-7129cd/) - Learn how to apply solid color fills to design elements using RGB, CMYK, and Spot Colors in CE.SDK - [Gradient Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/gradient-0ff079/) - Learn how to create and apply linear, radial, and conical gradient fills to design elements in CE.SDK - [Image Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/image-e9cb5c/) - Apply photos, textures, and patterns to design elements using image fills in CE.SDK. - [Video Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/video-ec7f9f/) - Apply motion content to design elements by filling shapes, backgrounds, and text with videos using CE.SDK's video fill 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: "Color Fills" description: "Learn how to apply solid color fills to design elements using RGB, CMYK, and Spot Colors in CE.SDK" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/fills/color-7129cd/" --- > 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/) > [Fills](https://img.ly/docs/cesdk/mac-catalyst/fills-402ddc/) > [Solid Color](https://img.ly/docs/cesdk/mac-catalyst/fills/color-7129cd/) --- ```swift file=@cesdk_swift_examples/engine-guides-fills-color/FillsColor.swift reference-only import IMGLYEngine @MainActor func fillsColor(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) guard try engine.block.supportsFill(page) else { return } let colorFill = try engine.block.createFill(.color) let allFillProperties = try engine.block.findAllProperties(colorFill) print("Fill properties:", allFillProperties) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 200) try engine.block.setHeight(block, value: 150) try engine.block.setPositionX(block, value: 50) try engine.block.setPositionY(block, value: 50) try engine.block.appendChild(to: page, child: block) try engine.block.setFill(block, fill: colorFill) try engine.block.setColor( colorFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.0, b: 0.0), ) let currentFill = try engine.block.getFill(block) let fillType = try engine.block.getType(currentFill) print("Fill type:", fillType) let currentColor: Color = try engine.block.getColor(colorFill, property: "fill/color/value") print("Current color:", currentColor) let cmykBlock = try engine.block.create(.graphic) try engine.block.setShape(cmykBlock, shape: engine.block.createShape(.ellipse)) try engine.block.setWidth(cmykBlock, value: 150) try engine.block.setHeight(cmykBlock, value: 150) try engine.block.setPositionX(cmykBlock, value: 300) try engine.block.setPositionY(cmykBlock, value: 50) try engine.block.appendChild(to: page, child: cmykBlock) let cmykFill = try engine.block.createFill(.color) try engine.block.setFill(cmykBlock, fill: cmykFill) try engine.block.setColor( cmykFill, property: "fill/color/value", color: .cmyk(c: 0.0, m: 1.0, y: 0.0, k: 0.0), ) engine.editor.setSpotColor(name: "BrandRed", r: 0.9, g: 0.1, b: 0.1) let spotBlock = try engine.block.create(.graphic) try engine.block.setShape(spotBlock, shape: engine.block.createShape(.ellipse)) try engine.block.setWidth(spotBlock, value: 150) try engine.block.setHeight(spotBlock, value: 150) try engine.block.setPositionX(spotBlock, value: 500) try engine.block.setPositionY(spotBlock, value: 50) try engine.block.appendChild(to: page, child: spotBlock) let spotFill = try engine.block.createFill(.color) try engine.block.setFill(spotBlock, fill: spotFill) try engine.block.setColor( spotFill, property: "fill/color/value", color: .spot(name: "BrandRed", externalReference: "Brand-Colors"), ) let toggleBlock = try engine.block.create(.graphic) try engine.block.setShape(toggleBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(toggleBlock, value: 150) try engine.block.setHeight(toggleBlock, value: 100) try engine.block.setPositionX(toggleBlock, value: 50) try engine.block.setPositionY(toggleBlock, value: 250) try engine.block.appendChild(to: page, child: toggleBlock) let toggleFill = try engine.block.createFill(.color) try engine.block.setFill(toggleBlock, fill: toggleFill) try engine.block.setColor( toggleFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.5, b: 0.0), ) let isEnabled = try engine.block.isFillEnabled(toggleBlock) print("Fill enabled:", isEnabled) try engine.block.setFillEnabled(toggleBlock, enabled: false) try engine.block.setFillEnabled(toggleBlock, enabled: true) let block1 = try engine.block.create(.graphic) try engine.block.setShape(block1, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block1, value: 100) try engine.block.setHeight(block1, value: 100) try engine.block.setPositionX(block1, value: 250) try engine.block.setPositionY(block1, value: 250) try engine.block.appendChild(to: page, child: block1) let block2 = try engine.block.create(.graphic) try engine.block.setShape(block2, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block2, value: 100) try engine.block.setHeight(block2, value: 100) try engine.block.setPositionX(block2, value: 370) try engine.block.setPositionY(block2, value: 250) try engine.block.appendChild(to: page, child: block2) let sharedFill = try engine.block.createFill(.color) try engine.block.setColor( sharedFill, property: "fill/color/value", color: .rgba(r: 0.5, g: 0.0, b: 0.5), ) try engine.block.setFill(block1, fill: sharedFill) try engine.block.setFill(block2, fill: sharedFill) try engine.block.setColor( sharedFill, property: "fill/color/value", color: .rgba(r: 0.0, g: 0.5, b: 0.5), ) let rgbColor = Color.rgba(r: 1.0, g: 0.0, b: 0.0) let cmykColor = try engine.editor.convertColorToColorSpace(color: rgbColor, colorSpace: .cmyk) print("Converted CMYK color:", cmykColor) engine.editor.setSpotColor(name: "PrimaryBrand", r: 0.2, g: 0.4, b: 0.8) engine.editor.setSpotColor(name: "SecondaryBrand", r: 0.9, g: 0.5, b: 0.1) let brandBlock = try engine.block.create(.graphic) try engine.block.setShape(brandBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(brandBlock, value: 150) try engine.block.setHeight(brandBlock, value: 100) try engine.block.setPositionX(brandBlock, value: 500) try engine.block.setPositionY(brandBlock, value: 250) try engine.block.appendChild(to: page, child: brandBlock) let brandFill = try engine.block.createFill(.color) try engine.block.setFill(brandBlock, fill: brandFill) try engine.block.setColor( brandFill, property: "fill/color/value", color: .spot(name: "PrimaryBrand"), ) let transparentBlock = try engine.block.create(.graphic) try engine.block.setShape(transparentBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(transparentBlock, value: 150) try engine.block.setHeight(transparentBlock, value: 100) try engine.block.setPositionX(transparentBlock, value: 50) try engine.block.setPositionY(transparentBlock, value: 400) try engine.block.appendChild(to: page, child: transparentBlock) let transparentFill = try engine.block.createFill(.color) try engine.block.setFill(transparentBlock, fill: transparentFill) try engine.block.setColor( transparentFill, property: "fill/color/value", color: .rgba(r: 0.0, g: 0.8, b: 0.2, a: 0.5), ) let printBlock = try engine.block.create(.graphic) try engine.block.setShape(printBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(printBlock, value: 150) try engine.block.setHeight(printBlock, value: 100) try engine.block.setPositionX(printBlock, value: 250) try engine.block.setPositionY(printBlock, value: 400) try engine.block.appendChild(to: page, child: printBlock) let printFill = try engine.block.createFill(.color) try engine.block.setFill(printBlock, fill: printFill) try engine.block.setColor( printFill, property: "fill/color/value", color: .cmyk(c: 0.0, m: 0.85, y: 1.0, k: 0.0), ) try await engine.captureGuide(page, label: "hero") } ``` Apply uniform solid colors to shapes, text, and design blocks using CE.SDK's comprehensive color fill system with support for multiple color spaces. ![Color fills applied to shapes using RGB, CMYK, and Spot Colors](./assets/swift-based.hero.webp) > **Reading time:** 15 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-fills-color) Color fills are one of the fundamental fill types in CE.SDK, allowing you to paint design blocks with solid, uniform colors. Unlike gradient fills that transition between colors or image fills that display photo content, color fills apply a single color across the entire block. The color fill system supports multiple color spaces including RGB for screen display, CMYK for print workflows, and Spot Colors for brand consistency. This guide demonstrates how to create, apply, and modify color fills programmatically, work with different color spaces, and manage fill properties for various design elements. ## Understanding Color Fills ### What is a Color Fill? A color fill is a fill object identified by the type `.color` (or the full form `"//ly.img.ubq/fill/color"`) that paints a design block with a single, uniform color. Color fills are part of the broader fill system in CE.SDK and contain a `"fill/color/value"` property that defines the actual color using various color space formats. Color fills differ from other fill types available in CE.SDK: - **Color fills**: Solid, uniform color across the entire block - **Gradient fills**: Color transitions (linear, radial, conic) - **Image fills**: Photo or raster content - **Video fills**: Animated video content ### Supported Color Spaces CE.SDK's color fill system supports multiple color spaces to accommodate different design and production workflows: - **RGB/sRGB**: Red, Green, Blue with alpha channel (standard for screen display) — `Color.rgba(r:g:b:a:)` - **CMYK**: Cyan, Magenta, Yellow, Key (black) with tint (for print production) — `Color.cmyk(c:m:y:k:tint:)` - **Spot Colors**: Named colors with RGB/CMYK approximations (for brand consistency) — `Color.spot(name:tint:externalReference:)` Each color space serves specific use cases — use RGB for digital designs, CMYK for print-ready content, and Spot Colors to maintain brand standards across projects. ## Checking Color Fill Support ### Verifying Block Compatibility Before applying color fills to a block, verify that the block type supports fills. Not all block types can have fills — for example, scene blocks typically don't support fills. ```swift highlight-fillsColor-checkFillSupport guard try engine.block.supportsFill(page) else { return } ``` Graphic blocks, shapes, and text blocks typically support fills. Always check `supportsFill(_:)` before accessing fill APIs to avoid runtime errors. ## Creating Color Fills ### Creating a New Color Fill Create a new color fill instance using `createFill(.color)`: ```swift highlight-fillsColor-createFill let colorFill = try engine.block.createFill(.color) ``` The `createFill(_:)` method returns a `DesignBlockID`. The fill exists independently until you attach it to a block using `setFill(_:fill:)`. If you create a fill but don't attach it to a block, you must destroy it manually with `destroy(_:)` to prevent memory leaks. ### Default Color Fill Properties New color fills start with a default black color at full opacity. Use `findAllProperties(_:)` to discover the available properties on a color fill: ```swift highlight-fillsColor-defaultProperties let allFillProperties = try engine.block.findAllProperties(colorFill) print("Fill properties:", allFillProperties) ``` The returned array includes `"fill/color/value"` — the property key used with `setColor(_:property:color:)` and `getColor(_:property:)` throughout this guide. ## Applying Color Fills ### Setting a Fill on a Block Once you've created a color fill, attach it to a block using `setFill(_:fill:)`: ```swift highlight-fillsColor-applyFill let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 200) try engine.block.setHeight(block, value: 150) try engine.block.setPositionX(block, value: 50) try engine.block.setPositionY(block, value: 50) try engine.block.appendChild(to: page, child: block) try engine.block.setFill(block, fill: colorFill) ``` This example creates a graphic block with a rectangle shape and applies the color fill to it. The block renders with the fill's color. ### Getting the Current Fill Retrieve the current fill attached to a block using `getFill(_:)` and inspect its type: ```swift highlight-fillsColor-getFill let currentFill = try engine.block.getFill(block) let fillType = try engine.block.getType(currentFill) print("Fill type:", fillType) ``` ## Modifying Color Fill Properties ### Setting RGB Colors Set the fill color using RGB values with `setColor(_:property:color:)`. RGB values are normalized floats from 0.0 to 1.0, and the alpha channel controls opacity. ```swift highlight-fillsColor-setRgb try engine.block.setColor( colorFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.0, b: 0.0), ) ``` The alpha channel (`a`) controls opacity: 1.0 is fully opaque, 0.0 is fully transparent. Both default to 1.0 when omitted. This allows you to create semi-transparent overlays and layered color effects. ### Setting CMYK Colors For print workflows, use CMYK color space with `setColor(_:property:color:)`. CMYK values are also normalized from 0.0 to 1.0, and include a tint value for partial color application. ```swift highlight-fillsColor-setCmyk let cmykBlock = try engine.block.create(.graphic) try engine.block.setShape(cmykBlock, shape: engine.block.createShape(.ellipse)) try engine.block.setWidth(cmykBlock, value: 150) try engine.block.setHeight(cmykBlock, value: 150) try engine.block.setPositionX(cmykBlock, value: 300) try engine.block.setPositionY(cmykBlock, value: 50) try engine.block.appendChild(to: page, child: cmykBlock) let cmykFill = try engine.block.createFill(.color) try engine.block.setFill(cmykBlock, fill: cmykFill) try engine.block.setColor( cmykFill, property: "fill/color/value", color: .cmyk(c: 0.0, m: 1.0, y: 0.0, k: 0.0), ) ``` The tint value allows partial application of the color, useful for creating lighter variations without changing the base CMYK values. It defaults to 1.0 when omitted. ### Setting Spot Colors Spot colors are named colors that must be defined before use. They're ideal for maintaining brand consistency and can have both RGB and CMYK approximations for different output scenarios. ```swift highlight-fillsColor-setSpot engine.editor.setSpotColor(name: "BrandRed", r: 0.9, g: 0.1, b: 0.1) let spotBlock = try engine.block.create(.graphic) try engine.block.setShape(spotBlock, shape: engine.block.createShape(.ellipse)) try engine.block.setWidth(spotBlock, value: 150) try engine.block.setHeight(spotBlock, value: 150) try engine.block.setPositionX(spotBlock, value: 500) try engine.block.setPositionY(spotBlock, value: 50) try engine.block.appendChild(to: page, child: spotBlock) let spotFill = try engine.block.createFill(.color) try engine.block.setFill(spotBlock, fill: spotFill) try engine.block.setColor( spotFill, property: "fill/color/value", color: .spot(name: "BrandRed", externalReference: "Brand-Colors"), ) ``` First, define the spot color globally using `engine.editor.setSpotColor(name:r:g:b:)` or `engine.editor.setSpotColor(name:c:m:y:k:)`, then apply it to your fill using `Color.spot(name:tint:externalReference:)`. The `externalReference` parameter identifies the color in an external named-color system (for example, a print vendor's library or your in-house brand palette); omit it when you don't need this mapping. The tint value controls intensity from 0.0 to 1.0. ### Getting Current Color Value Retrieve the current color value from a fill using `getColor(_:property:)`: ```swift highlight-fillsColor-getColor let currentColor: Color = try engine.block.getColor(colorFill, property: "fill/color/value") print("Current color:", currentColor) ``` The returned `Color` enum value preserves the original color space — an RGB color returns as `.rgba(...)`, a CMYK color as `.cmyk(...)`, and a Spot Color as `.spot(...)`. ## Enabling and Disabling Color Fills ### Toggle Fill Visibility You can temporarily disable a fill without removing it from the block. This preserves all fill properties while making the block transparent: ```swift highlight-fillsColor-toggleFill let toggleBlock = try engine.block.create(.graphic) try engine.block.setShape(toggleBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(toggleBlock, value: 150) try engine.block.setHeight(toggleBlock, value: 100) try engine.block.setPositionX(toggleBlock, value: 50) try engine.block.setPositionY(toggleBlock, value: 250) try engine.block.appendChild(to: page, child: toggleBlock) let toggleFill = try engine.block.createFill(.color) try engine.block.setFill(toggleBlock, fill: toggleFill) try engine.block.setColor( toggleFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.5, b: 0.0), ) let isEnabled = try engine.block.isFillEnabled(toggleBlock) print("Fill enabled:", isEnabled) try engine.block.setFillEnabled(toggleBlock, enabled: false) try engine.block.setFillEnabled(toggleBlock, enabled: true) ``` Disabling fills is useful for creating stroke-only designs or for temporarily hiding fills during interactive editing sessions. The fill properties remain intact and can be re-enabled at any time. ## Additional Techniques ### Sharing Color Fills You can share a single fill instance between multiple blocks. Changes to the shared fill affect all blocks using it: ```swift highlight-fillsColor-shareFill let block1 = try engine.block.create(.graphic) try engine.block.setShape(block1, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block1, value: 100) try engine.block.setHeight(block1, value: 100) try engine.block.setPositionX(block1, value: 250) try engine.block.setPositionY(block1, value: 250) try engine.block.appendChild(to: page, child: block1) let block2 = try engine.block.create(.graphic) try engine.block.setShape(block2, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block2, value: 100) try engine.block.setHeight(block2, value: 100) try engine.block.setPositionX(block2, value: 370) try engine.block.setPositionY(block2, value: 250) try engine.block.appendChild(to: page, child: block2) let sharedFill = try engine.block.createFill(.color) try engine.block.setColor( sharedFill, property: "fill/color/value", color: .rgba(r: 0.5, g: 0.0, b: 0.5), ) try engine.block.setFill(block1, fill: sharedFill) try engine.block.setFill(block2, fill: sharedFill) try engine.block.setColor( sharedFill, property: "fill/color/value", color: .rgba(r: 0.0, g: 0.5, b: 0.5), ) ``` With shared fills, modifying the fill's color updates all blocks simultaneously. Note that `setFill(_:fill:)` does not automatically destroy the previous fill on the target block — you must call `destroy(_:)` on replaced fills manually if they are no longer needed. ### Color Space Conversion Convert colors between different color spaces using `convertColorToColorSpace(color:colorSpace:)`: ```swift highlight-fillsColor-convertColor let rgbColor = Color.rgba(r: 1.0, g: 0.0, b: 0.0) let cmykColor = try engine.editor.convertColorToColorSpace(color: rgbColor, colorSpace: .cmyk) print("Converted CMYK color:", cmykColor) ``` This is useful when you need to ensure color consistency across different output mediums (screen vs. print). The `ColorSpace` enum provides `.sRGB`, `.cmyk`, and `.spotColor` cases. ## Common Use Cases ### Brand Color Application Define a palette of spot colors up front, then apply them across multiple design elements. Updating a spot color definition later automatically changes every fill that references it: ```swift highlight-fillsColor-brandColors engine.editor.setSpotColor(name: "PrimaryBrand", r: 0.2, g: 0.4, b: 0.8) engine.editor.setSpotColor(name: "SecondaryBrand", r: 0.9, g: 0.5, b: 0.1) let brandBlock = try engine.block.create(.graphic) try engine.block.setShape(brandBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(brandBlock, value: 150) try engine.block.setHeight(brandBlock, value: 100) try engine.block.setPositionX(brandBlock, value: 500) try engine.block.setPositionY(brandBlock, value: 250) try engine.block.appendChild(to: page, child: brandBlock) let brandFill = try engine.block.createFill(.color) try engine.block.setFill(brandBlock, fill: brandFill) try engine.block.setColor( brandFill, property: "fill/color/value", color: .spot(name: "PrimaryBrand"), ) ``` ### Transparency Effects Create semi-transparent overlays and visual effects by adjusting the alpha channel: ```swift highlight-fillsColor-transparency let transparentBlock = try engine.block.create(.graphic) try engine.block.setShape(transparentBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(transparentBlock, value: 150) try engine.block.setHeight(transparentBlock, value: 100) try engine.block.setPositionX(transparentBlock, value: 50) try engine.block.setPositionY(transparentBlock, value: 400) try engine.block.appendChild(to: page, child: transparentBlock) let transparentFill = try engine.block.createFill(.color) try engine.block.setFill(transparentBlock, fill: transparentFill) try engine.block.setColor( transparentFill, property: "fill/color/value", color: .rgba(r: 0.0, g: 0.8, b: 0.2, a: 0.5), ) ``` ### Print-Ready Colors Use CMYK color space for designs destined for print production: ```swift highlight-fillsColor-printColors let printBlock = try engine.block.create(.graphic) try engine.block.setShape(printBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(printBlock, value: 150) try engine.block.setHeight(printBlock, value: 100) try engine.block.setPositionX(printBlock, value: 250) try engine.block.setPositionY(printBlock, value: 400) try engine.block.appendChild(to: page, child: printBlock) let printFill = try engine.block.createFill(.color) try engine.block.setFill(printBlock, fill: printFill) try engine.block.setColor( printFill, property: "fill/color/value", color: .cmyk(c: 0.0, m: 0.85, y: 1.0, k: 0.0), ) ``` ## Troubleshooting ### Fill Not Visible If your fill doesn't appear: - Check if fill is enabled: `engine.block.isFillEnabled(block)` - Verify alpha channel is not 0: check the `a` parameter in `.rgba(...)` colors - Ensure block has valid dimensions (width and height > 0) - Confirm block is in the scene hierarchy ### Color Looks Different Than Expected If colors don't match expectations: - Verify you're using the correct color space (`.rgba` vs `.cmyk`) - Check if spot color is properly defined before use - Review tint values (should be 0.0–1.0) - Consider color space conversion for your output medium ### Memory Leaks To prevent memory leaks: - Always destroy replaced fills: `engine.block.destroy(oldFill)` - Don't create fills without attaching them to blocks - Clean up shared fills when they're no longer needed ### Cannot Apply Color to Block If you can't apply a color fill: - Verify block supports fills: `engine.block.supportsFill(block)` - Check if block has a shape: some blocks require shapes before fills work - Ensure fill object is valid and not already destroyed ## API Reference | Method | Description | | --- | --- | | `engine.block.createFill(.color)` | Create a new color fill object | | `engine.block.setFill(_:fill:)` | Assign fill to a block | | `engine.block.getFill(_:)` | Get the fill ID from a block | | `engine.block.setColor(_:property:color:)` | Set color value (`Color.rgba`, `.cmyk`, or `.spot`) | | `engine.block.getColor(_:property:) -> Color` | Get current color value | | `engine.block.setFillEnabled(_:enabled:)` | Enable or disable fill rendering | | `engine.block.isFillEnabled(_:)` | Check if fill is enabled | | `engine.block.supportsFill(_:)` | Check if block supports fills | | `engine.block.findAllProperties(_:)` | List all properties of the fill | | `engine.editor.convertColorToColorSpace(color:colorSpace:)` | Convert between color spaces | | `engine.editor.setSpotColor(name:r:g:b:)` | Define spot color with RGB approximation | | `engine.editor.setSpotColor(name:c:m:y:k:)` | Define spot color with CMYK approximation | ## Next Steps Now that you understand color fills, explore other fill types and color management features: - [Fills Overview](https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/) — Understand the comprehensive fill system and all available fill types - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) — Learn about color management across fills, strokes, and shadows - [Gradient Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/gradient-0ff079/) — Create color transitions with linear, radial, and conical gradients - [Image Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/image-e9cb5c/) — Display photo and raster content in 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: "Gradient Fills" description: "Learn how to create and apply linear, radial, and conical gradient fills to design elements in CE.SDK" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/fills/gradient-0ff079/" --- > 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/) > [Fills](https://img.ly/docs/cesdk/mac-catalyst/fills-402ddc/) > [Gradient](https://img.ly/docs/cesdk/mac-catalyst/fills/gradient-0ff079/) --- ```swift file=@cesdk_swift_examples/engine-guides-fills-gradient/FillsGradient.swift reference-only import IMGLYEngine @MainActor func fillsGradient(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) guard try engine.block.supportsFill(page) else { return } // Helper to create a renderable graphic block with a rect shape on the page. func createBlock( x: Float, y: Float, width: Float = 120, height: Float = 100, ) throws -> DesignBlockID { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) 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) return block } // ========================================================================= // 1 - Linear Gradient (Vertical: gold to blue) // ========================================================================= let linearFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( linearFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1.0, g: 0.8, b: 0.2), stop: 0), GradientColorStop(color: .rgba(r: 0.3, g: 0.4, b: 0.7), stop: 1), ], ) try engine.block.setFloat(linearFill, property: "fill/gradient/linear/startPointX", value: 0.5) try engine.block.setFloat(linearFill, property: "fill/gradient/linear/startPointY", value: 0) try engine.block.setFloat(linearFill, property: "fill/gradient/linear/endPointX", value: 0.5) try engine.block.setFloat(linearFill, property: "fill/gradient/linear/endPointY", value: 1) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let gradientFill = try engine.block.createFill(.linearGradient) try engine.block.setFill(block, fill: gradientFill) try engine.block.destroy(block) let linearBlock = try createBlock(x: 20, y: 20) try engine.block.setFill(linearBlock, fill: linearFill) // ========================================================================= // 2 - Linear Gradient (Horizontal: pink to teal) // ========================================================================= let horizontalFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( horizontalFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.8, g: 0.2, b: 0.4), stop: 0), GradientColorStop(color: .rgba(r: 0.2, g: 0.8, b: 0.6), stop: 1), ], ) try engine.block.setFloat(horizontalFill, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(horizontalFill, property: "fill/gradient/linear/startPointY", value: 0.5) try engine.block.setFloat(horizontalFill, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(horizontalFill, property: "fill/gradient/linear/endPointY", value: 0.5) let horizontalBlock = try createBlock(x: 160, y: 20) try engine.block.setFill(horizontalBlock, fill: horizontalFill) // ========================================================================= // 3 - Linear Gradient (Diagonal: purple to orange) // ========================================================================= let diagonalFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( diagonalFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.5, g: 0.2, b: 0.8), stop: 0), GradientColorStop(color: .rgba(r: 0.9, g: 0.6, b: 0.2), stop: 1), ], ) try engine.block.setFloat(diagonalFill, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(diagonalFill, property: "fill/gradient/linear/startPointY", value: 0) try engine.block.setFloat(diagonalFill, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(diagonalFill, property: "fill/gradient/linear/endPointY", value: 1) let diagonalBlock = try createBlock(x: 300, y: 20) try engine.block.setFill(diagonalBlock, fill: diagonalFill) // ========================================================================= // 4 - Aurora Multi-Stop Linear Gradient (purple -> pink -> orange -> gold) // ========================================================================= let auroraFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( auroraFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.4, g: 0.1, b: 0.8), stop: 0), GradientColorStop(color: .rgba(r: 0.8, g: 0.2, b: 0.6), stop: 0.3), GradientColorStop(color: .rgba(r: 1.0, g: 0.5, b: 0.3), stop: 0.6), GradientColorStop(color: .rgba(r: 1.0, g: 0.8, b: 0.2), stop: 1), ], ) try engine.block.setFloat(auroraFill, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(auroraFill, property: "fill/gradient/linear/startPointY", value: 0.5) try engine.block.setFloat(auroraFill, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(auroraFill, property: "fill/gradient/linear/endPointY", value: 0.5) let auroraBlock = try createBlock(x: 440, y: 20) try engine.block.setFill(auroraBlock, fill: auroraFill) // ========================================================================= // 5 - Radial Gradient (Centered: white translucent to blue) // ========================================================================= let radialFill = try engine.block.createFill(.radialGradient) try engine.block.setGradientColorStops( radialFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1.0, g: 1.0, b: 1.0, a: 0.3), stop: 0), GradientColorStop(color: .rgba(r: 0.2, g: 0.4, b: 0.8), stop: 1), ], ) try engine.block.setFloat(radialFill, property: "fill/gradient/radial/centerPointX", value: 0.5) try engine.block.setFloat(radialFill, property: "fill/gradient/radial/centerPointY", value: 0.5) try engine.block.setFloat(radialFill, property: "fill/gradient/radial/radius", value: 0.8) let radialBlock = try createBlock(x: 580, y: 20) try engine.block.setFill(radialBlock, fill: radialFill) // ========================================================================= // 6 - Radial Gradient (Top-Left Highlight / Button Effect) // ========================================================================= let buttonFill = try engine.block.createFill(.radialGradient) try engine.block.setGradientColorStops( buttonFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1.0, g: 1.0, b: 1.0, a: 0.3), stop: 0), GradientColorStop(color: .rgba(r: 0.2, g: 0.4, b: 0.8), stop: 1), ], ) try engine.block.setFloat(radialFill, property: "fill/gradient/radial/centerPointX", value: 0.5) try engine.block.setFloat(radialFill, property: "fill/gradient/radial/centerPointY", value: 0.5) try engine.block.setFloat(radialFill, property: "fill/gradient/radial/radius", value: 0.7) try engine.block.setFloat(buttonFill, property: "fill/gradient/radial/centerPointX", value: 0) try engine.block.setFloat(buttonFill, property: "fill/gradient/radial/centerPointY", value: 0) try engine.block.setFloat(buttonFill, property: "fill/gradient/radial/radius", value: 1.0) let buttonBlock = try createBlock(x: 20, y: 140) try engine.block.setFill(buttonBlock, fill: buttonFill) // ========================================================================= // 7 - Radial Gradient (Vignette: light center to dark edge) // ========================================================================= let vignetteFill = try engine.block.createFill(.radialGradient) try engine.block.setGradientColorStops( vignetteFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.9, g: 0.9, b: 0.9), stop: 0), GradientColorStop(color: .rgba(r: 0.1, g: 0.1, b: 0.1), stop: 1), ], ) try engine.block.setFloat(vignetteFill, property: "fill/gradient/radial/centerPointX", value: 1) try engine.block.setFloat(vignetteFill, property: "fill/gradient/radial/centerPointY", value: 1) try engine.block.setFloat(vignetteFill, property: "fill/gradient/radial/radius", value: 1.5) let vignetteBlock = try createBlock(x: 160, y: 140) try engine.block.setFill(vignetteBlock, fill: vignetteFill) // ========================================================================= // 8 - Conical Gradient (Color Wheel: red -> yellow -> green -> blue -> red) // ========================================================================= let conicalFill = try engine.block.createFill(.conicalGradient) try engine.block.setGradientColorStops( conicalFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1.0, g: 0.0, b: 0.0), stop: 0), GradientColorStop(color: .rgba(r: 1.0, g: 1.0, b: 0.0), stop: 0.25), GradientColorStop(color: .rgba(r: 0.0, g: 1.0, b: 0.0), stop: 0.5), GradientColorStop(color: .rgba(r: 0.0, g: 0.0, b: 1.0), stop: 0.75), GradientColorStop(color: .rgba(r: 1.0, g: 0.0, b: 0.0), stop: 1), ], ) try engine.block.setFloat(conicalFill, property: "fill/gradient/conical/centerPointX", value: 0.5) try engine.block.setFloat(conicalFill, property: "fill/gradient/conical/centerPointY", value: 0.5) let conicalBlock = try createBlock(x: 300, y: 140) try engine.block.setFill(conicalBlock, fill: conicalFill) // ========================================================================= // 9 - Conical Gradient (Spinner: blue -> transparent -> blue) // ========================================================================= let spinnerFill = try engine.block.createFill(.conicalGradient) try engine.block.setGradientColorStops( spinnerFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.2, g: 0.4, b: 0.8), stop: 0), GradientColorStop(color: .rgba(r: 0.2, g: 0.4, b: 0.8, a: 0), stop: 0.75), GradientColorStop(color: .rgba(r: 0.2, g: 0.4, b: 0.8), stop: 1), ], ) try engine.block.setFloat(spinnerFill, property: "fill/gradient/conical/centerPointX", value: 0.5) try engine.block.setFloat(spinnerFill, property: "fill/gradient/conical/centerPointY", value: 0.5) let spinnerBlock = try createBlock(x: 440, y: 140) try engine.block.setFill(spinnerBlock, fill: spinnerFill) // ========================================================================= // 10 - CMYK Gradient (magenta-yellow to cyan-yellow) // ========================================================================= let cmykFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( cmykFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .cmyk(c: 0.0, m: 1.0, y: 1.0, k: 0.0), stop: 0), GradientColorStop(color: .cmyk(c: 1.0, m: 0.0, y: 1.0, k: 0.0), stop: 1), ], ) engine.editor.setSpotColor(name: "BrandPrimary", r: 0.2, g: 0.4, b: 0.8) try engine.block.setGradientColorStops( cmykFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .spot(name: "BrandPrimary"), stop: 0), GradientColorStop(color: .rgba(r: 1.0, g: 1.0, b: 1.0), stop: 1), ], ) try engine.block.setFloat(cmykFill, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(cmykFill, property: "fill/gradient/linear/startPointY", value: 0.5) try engine.block.setFloat(cmykFill, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(cmykFill, property: "fill/gradient/linear/endPointY", value: 0.5) let cmykBlock = try createBlock(x: 580, y: 140) try engine.block.setFill(cmykBlock, fill: cmykFill) // 11 - Spot Color Gradient (BrandPrimary to BrandSecondary) engine.editor.setSpotColor(name: "BrandSecondary", r: 1.0, g: 0.6, b: 0.0) let spotFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( spotFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .spot(name: "BrandPrimary"), stop: 0), GradientColorStop(color: .spot(name: "BrandSecondary"), stop: 1), ], ) try engine.block.setFloat(spotFill, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(spotFill, property: "fill/gradient/linear/startPointY", value: 0) try engine.block.setFloat(spotFill, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(spotFill, property: "fill/gradient/linear/endPointY", value: 1) let spotBlock = try createBlock(x: 20, y: 260) try engine.block.setFill(spotBlock, fill: spotFill) // ========================================================================= // 12 - Transparency Overlay (transparent to black 70%) // ========================================================================= let overlayFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( overlayFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 0), stop: 0), GradientColorStop(color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 0.7), stop: 1), ], ) try engine.block.setFloat(overlayFill, property: "fill/gradient/linear/startPointX", value: 0.5) try engine.block.setFloat(overlayFill, property: "fill/gradient/linear/startPointY", value: 0) try engine.block.setFloat(overlayFill, property: "fill/gradient/linear/endPointX", value: 0.5) try engine.block.setFloat(overlayFill, property: "fill/gradient/linear/endPointY", value: 1) let overlayBlock = try createBlock(x: 160, y: 260) try engine.block.setFill(overlayBlock, fill: overlayFill) // ========================================================================= // 13 - Duotone (purple to teal) // ========================================================================= let duotoneFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( duotoneFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.8, g: 0.2, b: 0.9), stop: 0), GradientColorStop(color: .rgba(r: 0.2, g: 0.9, b: 0.8), stop: 1), ], ) try engine.block.setFloat(duotoneFill, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(duotoneFill, property: "fill/gradient/linear/startPointY", value: 0) try engine.block.setFloat(duotoneFill, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(duotoneFill, property: "fill/gradient/linear/endPointY", value: 1) let duotoneBlock = try createBlock(x: 300, y: 260) try engine.block.setFill(duotoneBlock, fill: duotoneFill) // ========================================================================= // 14 - Shared Gradient (red to blue applied to 2 blocks, then updated) // ========================================================================= let sharedBlock1 = try createBlock(x: 440, y: 260, width: 120, height: 45) let sharedBlock2 = try createBlock(x: 440, y: 315, width: 120, height: 45) let sharedGradient = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( sharedGradient, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1, g: 0, b: 0), stop: 0), GradientColorStop(color: .rgba(r: 0, g: 0, b: 1), stop: 1), ], ) try engine.block.setFloat(sharedGradient, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(sharedGradient, property: "fill/gradient/linear/startPointY", value: 0.5) try engine.block.setFloat(sharedGradient, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(sharedGradient, property: "fill/gradient/linear/endPointY", value: 0.5) try engine.block.setFill(sharedBlock1, fill: sharedGradient) try engine.block.setFill(sharedBlock2, fill: sharedGradient) try engine.block.setGradientColorStops( sharedGradient, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0, g: 1, b: 0), stop: 0), GradientColorStop(color: .rgba(r: 1, g: 1, b: 0), stop: 1), ], ) // ========================================================================= // 15 - Inspect Gradient (get-fill and get-color-stops demos) // ========================================================================= let inspectBlock = try createBlock(x: 580, y: 260) let inspectFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( inspectFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.6, g: 0.3, b: 0.7), stop: 0), GradientColorStop(color: .rgba(r: 0.3, g: 0.7, b: 0.6), stop: 1), ], ) try engine.block.setFill(inspectBlock, fill: inspectFill) let currentFill = try engine.block.getFill(inspectBlock) let fillType = try engine.block.getType(currentFill) print("Fill type:", fillType) let colorStops = try engine.block.getGradientColorStops( inspectFill, property: "fill/gradient/colors", ) print("Color stops:", colorStops) let startX = try engine.block.getFloat(inspectFill, property: "fill/gradient/linear/startPointX") let startY = try engine.block.getFloat(inspectFill, property: "fill/gradient/linear/startPointY") let endX = try engine.block.getFloat(inspectFill, property: "fill/gradient/linear/endPointX") let endY = try engine.block.getFloat(inspectFill, property: "fill/gradient/linear/endPointY") print("Linear gradient position:", startX, startY, endX, endY) try await engine.captureGuide(page, label: "hero") } ``` Create smooth color transitions in shapes, text, and design blocks using CE.SDK's gradient fill system with support for linear, radial, and conical gradients. ![Gradient fills applied to shapes using linear, radial, and conical gradients](./assets/swift-based.hero.webp) > **Reading time:** 20 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-fills-gradient) Gradient fills are one of the fundamental fill types in CE.SDK, allowing you to paint design blocks with smooth color transitions. Unlike solid color fills that apply a uniform color or image fills that display photo content, gradient fills create dynamic visual effects with depth and visual interest. The gradient fill system supports three types: linear gradients that transition along a straight line, radial gradients that emanate from a center point, and conical gradients that rotate around a center point like a color wheel. This guide demonstrates how to create, apply, and configure gradient fills programmatically, work with color stops, position gradients, and create modern visual effects like aurora gradients and button highlights. ## Understanding Gradient Fills ### What is a Gradient Fill? A gradient fill is a fill object that paints a design block with smooth color transitions. Gradient fills are part of the broader fill system in CE.SDK and come in three types, each identified by a `FillType` enum case: - **Linear**: `.linearGradient` (full form `"//ly.img.ubq/fill/gradient/linear"`) - **Radial**: `.radialGradient` (full form `"//ly.img.ubq/fill/gradient/radial"`) - **Conical**: `.conicalGradient` (full form `"//ly.img.ubq/fill/gradient/conical"`) Each gradient type contains color stops that define colors at specific positions and positioning properties that control the gradient's direction and coverage. ### Gradient Types Comparison #### Linear Gradients Linear gradients transition colors along a straight line defined by start and end points. They're the most common gradient type and create clean, modern looks. Common use cases include hero sections, call-to-action buttons, headers, and banners. ```swift highlight-fillsGradient-linearGradient try engine.block.setGradientColorStops( linearFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1.0, g: 0.8, b: 0.2), stop: 0), GradientColorStop(color: .rgba(r: 0.3, g: 0.4, b: 0.7), stop: 1), ], ) ``` #### Radial Gradients Radial gradients emanate from a central point outward, creating circular or elliptical color transitions. They add depth and create focal points or spotlight effects. Common use cases include button highlights, card shadows, vignettes, and circular badges. ```swift highlight-fillsGradient-radialGradient try engine.block.setGradientColorStops( radialFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1.0, g: 1.0, b: 1.0, a: 0.3), stop: 0), GradientColorStop(color: .rgba(r: 0.2, g: 0.4, b: 0.8), stop: 1), ], ) ``` #### Conical Gradients Conical gradients transition colors around a center point like a color wheel, starting at the top (12 o'clock) and rotating clockwise. Colors are specified by position rather than angle. Common use cases include pie charts, loading spinners, circular progress indicators, and color picker wheels. ```swift highlight-fillsGradient-conicalGradient try engine.block.setGradientColorStops( conicalFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1.0, g: 0.0, b: 0.0), stop: 0), GradientColorStop(color: .rgba(r: 1.0, g: 1.0, b: 0.0), stop: 0.25), GradientColorStop(color: .rgba(r: 0.0, g: 1.0, b: 0.0), stop: 0.5), GradientColorStop(color: .rgba(r: 0.0, g: 0.0, b: 1.0), stop: 0.75), GradientColorStop(color: .rgba(r: 1.0, g: 0.0, b: 0.0), stop: 1), ], ) ``` ### Gradient vs Other Fill Types Understanding how gradients differ from other fill types helps you choose the right fill for your design: - **Gradient fills**: Smooth color transitions (linear, radial, conical) - **Color fills**: Solid, uniform color - **Image fills**: Photo or raster content - **Video fills**: Animated video content ### Color Stops Explained Color stops define the colors at specific positions in the gradient. Each `GradientColorStop` consists of: - `color`: A `Color` value (`.rgba`, `.cmyk`, or `.spot`) - `stop`: Position value between 0.0 and 1.0 (0% to 100%) A gradient requires a minimum of two color stops. You can add multiple stops to create complex color transitions. Color stops can use any color space supported by CE.SDK, including RGB for screen display, CMYK for print, and Spot Colors for brand consistency. ```swift highlight-fillsGradient-colorStops try engine.block.setGradientColorStops( auroraFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.4, g: 0.1, b: 0.8), stop: 0), GradientColorStop(color: .rgba(r: 0.8, g: 0.2, b: 0.6), stop: 0.3), GradientColorStop(color: .rgba(r: 1.0, g: 0.5, b: 0.3), stop: 0.6), GradientColorStop(color: .rgba(r: 1.0, g: 0.8, b: 0.2), stop: 1), ], ) ``` ## Checking Gradient Fill Support ### Verifying Block Compatibility Before applying gradient fills, verify that the block type supports fills. Not all blocks support fills -- for example, scenes typically don't. ```swift highlight-fillsGradient-checkFillSupport guard try engine.block.supportsFill(page) else { return } ``` Always check `supportsFill(_:)` before accessing fill APIs. Graphic blocks, shapes, and text typically support fills. ## Creating Gradient Fills ### Creating a New Linear Gradient Create a new linear gradient fill using `createFill(.linearGradient)`: ```swift highlight-fillsGradient-createLinear let linearFill = try engine.block.createFill(.linearGradient) ``` ### Creating a Radial Gradient Create a radial gradient using `createFill(.radialGradient)`: ```swift highlight-fillsGradient-createRadial let radialFill = try engine.block.createFill(.radialGradient) ``` ### Creating a Conical Gradient Create a conical gradient using `createFill(.conicalGradient)`: ```swift highlight-fillsGradient-createConical let conicalFill = try engine.block.createFill(.conicalGradient) ``` The `createFill(_:)` method returns a `DesignBlockID`. The fill exists independently until you attach it to a block. If you create a fill but don't attach it to a block, you must destroy it manually with `destroy(_:)` to prevent memory leaks. ## Applying Gradient Fills ### Setting a Gradient Fill on a Block Once you've created a gradient fill, attach it to a block using `setFill(_:fill:)`: ```swift highlight-fillsGradient-applyGradient let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let gradientFill = try engine.block.createFill(.linearGradient) try engine.block.setFill(block, fill: gradientFill) ``` ### Getting the Current Fill Retrieve the current fill attached to a block and inspect its type: ```swift highlight-fillsGradient-getFill let currentFill = try engine.block.getFill(inspectBlock) let fillType = try engine.block.getType(currentFill) print("Fill type:", fillType) ``` ## Configuring Gradient Color Stops ### Setting Color Stops Set color stops using `setGradientColorStops(_:property:colors:)` with an array of `GradientColorStop` values: ```swift highlight-fillsGradient-linearGradient try engine.block.setGradientColorStops( linearFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1.0, g: 0.8, b: 0.2), stop: 0), GradientColorStop(color: .rgba(r: 0.3, g: 0.4, b: 0.7), stop: 1), ], ) ``` RGB values are normalized floats from 0.0 to 1.0. Stop positions are normalized where 0.0 represents the start and 1.0 represents the end. The alpha channel controls opacity per color stop and defaults to 1.0 when omitted. ### Getting Color Stops Retrieve the current color stops from a gradient fill: ```swift highlight-fillsGradient-getColorStops let colorStops = try engine.block.getGradientColorStops( inspectFill, property: "fill/gradient/colors", ) print("Color stops:", colorStops) ``` ### Using Different Color Spaces Gradient color stops support multiple color spaces: ```swift highlight-fillsGradient-colorSpaces try engine.block.setGradientColorStops( cmykFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .cmyk(c: 0.0, m: 1.0, y: 1.0, k: 0.0), stop: 0), GradientColorStop(color: .cmyk(c: 1.0, m: 0.0, y: 1.0, k: 0.0), stop: 1), ], ) engine.editor.setSpotColor(name: "BrandPrimary", r: 0.2, g: 0.4, b: 0.8) try engine.block.setGradientColorStops( cmykFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .spot(name: "BrandPrimary"), stop: 0), GradientColorStop(color: .rgba(r: 1.0, g: 1.0, b: 1.0), stop: 1), ], ) ``` ## Positioning Linear Gradients ### Setting Start and End Points Linear gradients are positioned using start and end points with normalized coordinates (0.0 to 1.0) relative to block dimensions: ```swift highlight-fillsGradient-linearPosition try engine.block.setFloat(linearFill, property: "fill/gradient/linear/startPointX", value: 0.5) try engine.block.setFloat(linearFill, property: "fill/gradient/linear/startPointY", value: 0) try engine.block.setFloat(linearFill, property: "fill/gradient/linear/endPointX", value: 0.5) try engine.block.setFloat(linearFill, property: "fill/gradient/linear/endPointY", value: 1) ``` Coordinates are normalized where (0, 0) represents the top-left corner and (1, 1) represents the bottom-right corner. ### Common Linear Gradient Directions **Horizontal (Left to Right):** ```swift highlight-fillsGradient-horizontalDirection try engine.block.setFloat(horizontalFill, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(horizontalFill, property: "fill/gradient/linear/startPointY", value: 0.5) try engine.block.setFloat(horizontalFill, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(horizontalFill, property: "fill/gradient/linear/endPointY", value: 0.5) ``` **Diagonal (Top-Left to Bottom-Right):** ```swift highlight-fillsGradient-diagonalDirection try engine.block.setFloat(diagonalFill, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(diagonalFill, property: "fill/gradient/linear/startPointY", value: 0) try engine.block.setFloat(diagonalFill, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(diagonalFill, property: "fill/gradient/linear/endPointY", value: 1) ``` ### Getting Current Position Retrieve the current position values: ```swift highlight-fillsGradient-getLinearPosition let startX = try engine.block.getFloat(inspectFill, property: "fill/gradient/linear/startPointX") let startY = try engine.block.getFloat(inspectFill, property: "fill/gradient/linear/startPointY") let endX = try engine.block.getFloat(inspectFill, property: "fill/gradient/linear/endPointX") let endY = try engine.block.getFloat(inspectFill, property: "fill/gradient/linear/endPointY") print("Linear gradient position:", startX, startY, endX, endY) ``` ## Positioning Radial Gradients ### Setting Center Point and Radius Radial gradients are positioned using a center point and radius: ```swift highlight-fillsGradient-radialPosition try engine.block.setFloat(radialFill, property: "fill/gradient/radial/centerPointX", value: 0.5) try engine.block.setFloat(radialFill, property: "fill/gradient/radial/centerPointY", value: 0.5) try engine.block.setFloat(radialFill, property: "fill/gradient/radial/radius", value: 0.8) ``` The `centerPointX/Y` properties use normalized coordinates (0.0 to 1.0) relative to block dimensions. The `radius` property is relative to the smaller side of the block frame, where 1.0 equals full coverage. Default values are centerX = 0.0, centerY = 0.0, and radius = 1.0. ### Common Radial Patterns **Centered Circle:** ```swift highlight-fillsGradient-centeredCircle try engine.block.setFloat(radialFill, property: "fill/gradient/radial/centerPointX", value: 0.5) try engine.block.setFloat(radialFill, property: "fill/gradient/radial/centerPointY", value: 0.5) try engine.block.setFloat(radialFill, property: "fill/gradient/radial/radius", value: 0.7) ``` **Top-Left Highlight:** ```swift highlight-fillsGradient-topLeftHighlight try engine.block.setFloat(buttonFill, property: "fill/gradient/radial/centerPointX", value: 0) try engine.block.setFloat(buttonFill, property: "fill/gradient/radial/centerPointY", value: 0) try engine.block.setFloat(buttonFill, property: "fill/gradient/radial/radius", value: 1.0) ``` **Bottom-Right Vignette:** ```swift highlight-fillsGradient-bottomRightVignette try engine.block.setFloat(vignetteFill, property: "fill/gradient/radial/centerPointX", value: 1) try engine.block.setFloat(vignetteFill, property: "fill/gradient/radial/centerPointY", value: 1) try engine.block.setFloat(vignetteFill, property: "fill/gradient/radial/radius", value: 1.5) ``` ## Positioning Conical Gradients ### Setting Center Point Conical gradients are positioned using a center point. The rotation starts at the top (12 o'clock) and proceeds clockwise: ```swift highlight-fillsGradient-conicalPosition try engine.block.setFloat(conicalFill, property: "fill/gradient/conical/centerPointX", value: 0.5) try engine.block.setFloat(conicalFill, property: "fill/gradient/conical/centerPointY", value: 0.5) ``` The `centerPointX/Y` properties use normalized coordinates (0.0 to 1.0) relative to block dimensions. There is no separate rotation or angle property -- the gradient always starts at the top. Default values are centerX = 0.0 and centerY = 0.0. ## Additional Techniques ### Sharing Gradient Fills You can share a single gradient fill between multiple blocks. Changes to the shared gradient affect all blocks using it. Note that `setFill(_:fill:)` does not automatically destroy the previous fill -- call `destroy(_:)` manually if the replaced fill is no longer needed. ```swift highlight-fillsGradient-shareGradient let sharedBlock1 = try createBlock(x: 440, y: 260, width: 120, height: 45) let sharedBlock2 = try createBlock(x: 440, y: 315, width: 120, height: 45) let sharedGradient = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( sharedGradient, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1, g: 0, b: 0), stop: 0), GradientColorStop(color: .rgba(r: 0, g: 0, b: 1), stop: 1), ], ) try engine.block.setFloat(sharedGradient, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(sharedGradient, property: "fill/gradient/linear/startPointY", value: 0.5) try engine.block.setFloat(sharedGradient, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(sharedGradient, property: "fill/gradient/linear/endPointY", value: 0.5) try engine.block.setFill(sharedBlock1, fill: sharedGradient) try engine.block.setFill(sharedBlock2, fill: sharedGradient) try engine.block.setGradientColorStops( sharedGradient, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0, g: 1, b: 0), stop: 0), GradientColorStop(color: .rgba(r: 1, g: 1, b: 0), stop: 1), ], ) ``` ### Duplicating Gradient Fills When you duplicate a block, its gradient fill is automatically duplicated, creating an independent copy. Each duplicate has its own fill instance that can be modified independently without affecting the original. ## Common Use Cases ### Modern Hero Background (Aurora Effect) Create dreamy multi-color gradient backgrounds for hero sections: ```swift highlight-fillsGradient-auroraGradient let auroraFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( auroraFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.4, g: 0.1, b: 0.8), stop: 0), GradientColorStop(color: .rgba(r: 0.8, g: 0.2, b: 0.6), stop: 0.3), GradientColorStop(color: .rgba(r: 1.0, g: 0.5, b: 0.3), stop: 0.6), GradientColorStop(color: .rgba(r: 1.0, g: 0.8, b: 0.2), stop: 1), ], ) try engine.block.setFloat(auroraFill, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(auroraFill, property: "fill/gradient/linear/startPointY", value: 0.5) try engine.block.setFloat(auroraFill, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(auroraFill, property: "fill/gradient/linear/endPointY", value: 0.5) ``` ### Button Highlight Effect Use radial gradients to add depth and highlight effects to buttons: ```swift highlight-fillsGradient-buttonGradient let buttonFill = try engine.block.createFill(.radialGradient) try engine.block.setGradientColorStops( buttonFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1.0, g: 1.0, b: 1.0, a: 0.3), stop: 0), GradientColorStop(color: .rgba(r: 0.2, g: 0.4, b: 0.8), stop: 1), ], ) ``` ### Loading Spinner (Conical) Create circular progress indicators and loading animations with conical gradients: ```swift highlight-fillsGradient-spinnerGradient let spinnerFill = try engine.block.createFill(.conicalGradient) try engine.block.setGradientColorStops( spinnerFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.2, g: 0.4, b: 0.8), stop: 0), GradientColorStop(color: .rgba(r: 0.2, g: 0.4, b: 0.8, a: 0), stop: 0.75), GradientColorStop(color: .rgba(r: 0.2, g: 0.4, b: 0.8), stop: 1), ], ) try engine.block.setFloat(spinnerFill, property: "fill/gradient/conical/centerPointX", value: 0.5) try engine.block.setFloat(spinnerFill, property: "fill/gradient/conical/centerPointY", value: 0.5) ``` ### Transparency Overlay Create smooth transparency effects with alpha channel transitions: ```swift highlight-fillsGradient-overlayGradient let overlayFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( overlayFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 0), stop: 0), GradientColorStop(color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 0.7), stop: 1), ], ) try engine.block.setFloat(overlayFill, property: "fill/gradient/linear/startPointX", value: 0.5) try engine.block.setFloat(overlayFill, property: "fill/gradient/linear/startPointY", value: 0) try engine.block.setFloat(overlayFill, property: "fill/gradient/linear/endPointX", value: 0.5) try engine.block.setFloat(overlayFill, property: "fill/gradient/linear/endPointY", value: 1) ``` ### Duotone Effect Create modern two-color gradient overlays: ```swift highlight-fillsGradient-duotoneGradient let duotoneFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( duotoneFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.8, g: 0.2, b: 0.9), stop: 0), GradientColorStop(color: .rgba(r: 0.2, g: 0.9, b: 0.8), stop: 1), ], ) try engine.block.setFloat(duotoneFill, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(duotoneFill, property: "fill/gradient/linear/startPointY", value: 0) try engine.block.setFloat(duotoneFill, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(duotoneFill, property: "fill/gradient/linear/endPointY", value: 1) ``` ## Troubleshooting ### Gradient Not Visible If your gradient doesn't appear: - Check if fill is enabled: `engine.block.isFillEnabled(block)` - Verify color stops have visible colors (check alpha channels) - Ensure block has valid dimensions (width and height > 0) - Confirm block is in the scene hierarchy - Check if color stops are properly ordered by stop position ### Gradient Looks Different Than Expected If the gradient doesn't look right: - Verify color stop positions are between 0.0 and 1.0 - Check gradient direction and positioning properties - Ensure correct gradient type is used (linear vs radial vs conical) - Review color space (`.rgba` vs `.cmyk`) for output medium - Confirm alpha values for transparency effects ### Gradient Direction Wrong If the gradient direction is incorrect: - For linear gradients, check `startPointX/Y` and `endPointX/Y` values - Remember coordinates are normalized (0.0 to 1.0), not pixels - Verify the block's coordinate system and transformations - Test with simple horizontal or vertical gradients first ### Memory Leaks To prevent memory leaks: - Always destroy replaced gradients: `engine.block.destroy(oldFill)` - Don't create gradient fills without attaching them to blocks - Clean up shared gradients when no longer needed ### Cannot Apply Gradient to Block If you can't apply a gradient fill: - Verify block supports fills: `engine.block.supportsFill(block)` - Check if block has a shape: some blocks require shapes - Ensure gradient fill object is valid and not already destroyed ### Color Stops Not Updating If color stops don't update: - Verify you're calling `setGradientColorStops(_:property:colors:)` not `setColor(_:property:color:)` - Ensure property name is exactly `"fill/gradient/colors"` - Check that the color stop array is properly formatted - Confirm fill ID is correct and still valid ## API Reference ### Core Methods | Method | Description | | --- | --- | | `engine.block.createFill(.linearGradient)` | Create a new linear gradient fill | | `engine.block.createFill(.radialGradient)` | Create a new radial gradient fill | | `engine.block.createFill(.conicalGradient)` | Create a new conical gradient fill | | `engine.block.setFill(_:fill:)` | Assign gradient fill to a block | | `engine.block.getFill(_:)` | Get the fill ID from a block | | `engine.block.setGradientColorStops(_:property:colors:)` | Set gradient color stops array | | `engine.block.getGradientColorStops(_:property:)` | Get current gradient color stops | | `engine.block.setFloat(_:property:value:)` | Set gradient position/radius properties | | `engine.block.getFloat(_:property:)` | Get gradient position/radius values | | `engine.block.setFillEnabled(_:enabled:)` | Enable or disable fill rendering | | `engine.block.isFillEnabled(_:)` | Check if fill is enabled | | `engine.block.supportsFill(_:)` | Check if block supports fills | ### Linear Gradient Properties | Property | Type | Default | Description | | --- | --- | --- | --- | | `fill/gradient/colors` | \[GradientColorStop] | - | Array of color stops | | `fill/gradient/linear/startPointX` | Float (0.0-1.0) | 0.5 | Horizontal start position | | `fill/gradient/linear/startPointY` | Float (0.0-1.0) | 0.0 | Vertical start position | | `fill/gradient/linear/endPointX` | Float (0.0-1.0) | 0.5 | Horizontal end position | | `fill/gradient/linear/endPointY` | Float (0.0-1.0) | 1.0 | Vertical end position | ### Radial Gradient Properties | Property | Type | Default | Description | | --- | --- | --- | --- | | `fill/gradient/colors` | \[GradientColorStop] | - | Array of color stops | | `fill/gradient/radial/centerPointX` | Float (0.0-1.0) | 0.0 | Horizontal center position | | `fill/gradient/radial/centerPointY` | Float (0.0-1.0) | 0.0 | Vertical center position | | `fill/gradient/radial/radius` | Float | 1.0 | Radius relative to smaller side | ### Conical Gradient Properties | Property | Type | Default | Description | | --- | --- | --- | --- | | `fill/gradient/colors` | \[GradientColorStop] | - | Array of color stops | | `fill/gradient/conical/centerPointX` | Float (0.0-1.0) | 0.0 | Horizontal center position | | `fill/gradient/conical/centerPointY` | Float (0.0-1.0) | 0.0 | Vertical center position | **Note**: Conical gradients rotate clockwise starting from the top (12 o'clock). There is no rotation or angle property. ### GradientColorStop Struct Each `GradientColorStop` has two fields: - `color: Color` — a `Color` enum value (`.rgba(...)`, `.cmyk(...)`, or `.spot(...)`) - `stop: Float` — position in the gradient from 0.0 (start) to 1.0 (end) ## Next Steps Now that you understand gradient fills, explore other fill types and color management features: - [Color Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/color-7129cd/) -- Learn about solid color fills with RGB, CMYK, and Spot Colors - [Image Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/image-e9cb5c/) -- Display photo and raster content in design blocks - [Fills Overview](https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/) -- Understand the comprehensive fill system and all available fill types - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) -- Learn about color management across fills, strokes, and shadows --- ## 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: "Image Fills" description: "Apply photos, textures, and patterns to design elements using image fills in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/fills/image-e9cb5c/" --- > 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/) > [Fills](https://img.ly/docs/cesdk/mac-catalyst/fills-402ddc/) > [Image](https://img.ly/docs/cesdk/mac-catalyst/fills/image-e9cb5c/) --- Fill graphic blocks with photos and images from URLs, data URIs, or asset libraries using CE.SDK's versatile image fill system. ![A square graphic block filled with an aerial photograph of an ocean coastline using image fill in Cover mode](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-fills-image) Image fills render design blocks with raster or vector image content, supporting common formats such as PNG, JPEG, WebP, and SVG. You can load images from remote URLs and data URIs, with built-in support for responsive images through source sets and selectable content fill modes that control how the image scales within its block. ```swift file=@cesdk_swift_examples/engine-guides-fills-image/FillsImage.swift reference-only import Foundation import IMGLYEngine @MainActor func fillsImage(engine: Engine) async throws { // Demo scaffolding: a scene with a page and a single graphic block to receive the image fill. 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: 500) try engine.block.setHeight(block, value: 500) try engine.block.setPositionX(block, value: 150) try engine.block.setPositionY(block, value: 50) try engine.block.appendChild(to: page, child: block) let baseURL = try engine.guidesBaseURL let imagesURL = baseURL.appendingPathComponent("ly.img.image/images") let sampleImageURL = imagesURL.appendingPathComponent("sample_1.jpg") let canHaveFill = try engine.block.supportsFill(block) print("Block supports fills: \(canHaveFill)") let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: sampleImageURL, ) try engine.block.setFill(block, fill: imageFill) let currentFill = try engine.block.getFill(block) let fillType = try engine.block.getType(currentFill) print("Fill type: \(fillType)") try engine.block.setEnum(block, property: "contentFill/mode", value: "Cover") try await engine.captureGuide(page, label: "after-cover") try engine.block.setEnum(block, property: "contentFill/mode", value: "Contain") try await engine.captureGuide(page, label: "after-contain") let currentMode = try engine.block.getEnum(block, property: "contentFill/mode") print("Current fill mode: \(currentMode)") try engine.block.setSourceSet( imageFill, property: "fill/image/sourceSet", sourceSet: [ Source(uri: imagesURL.appendingPathComponent("sample_1-512x341.jpg"), width: 512, height: 341), Source(uri: imagesURL.appendingPathComponent("sample_1-883x589.jpg"), width: 883, height: 589), Source(uri: imagesURL.appendingPathComponent("sample_1-1767x1178.jpg"), width: 1767, height: 1178), ], ) let sourceSet = try engine.block.getSourceSet(imageFill, property: "fill/image/sourceSet") print("Source set entries: \(sourceSet.count)") // Clear the source set so the engine falls back to the single imageFileURI // for the hero composition; the URI was never overwritten, so no need to re-set it. try engine.block.setSourceSet(imageFill, property: "fill/image/sourceSet", sourceSet: []) try await engine.captureGuide(page, label: "hero") let svgContent = """ \ \ """ let svgData = Data(svgContent.utf8).base64EncodedString() let svgDataUri = "data:image/svg+xml;base64,\(svgData)" let dataUriBlock = try engine.block.create(.graphic) try engine.block.setShape(dataUriBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(dataUriBlock, value: 120) try engine.block.setHeight(dataUriBlock, value: 120) try engine.block.setPositionX(dataUriBlock, value: 640) try engine.block.setPositionY(dataUriBlock, value: 60) try engine.block.appendChild(to: page, child: dataUriBlock) let dataUriFill = try engine.block.createFill(.image) try engine.block.setString(dataUriFill, property: "fill/image/imageFileURI", value: svgDataUri) try engine.block.setFill(dataUriBlock, fill: dataUriFill) try engine.block.setOpacity(dataUriBlock, value: 0.6) } ``` This guide covers how to create and apply image fills programmatically, configure content fill modes, work with responsive source sets, and load images from different sources. ## Understanding Image Fills Image fills are one of the fundamental fill types in CE.SDK, identified by the type string `"//ly.img.ubq/fill/image"` or the `FillType.image` case. While color fills produce solid colors and gradient fills produce color transitions, image fills display raster or vector content from image files. CE.SDK supports common image formats including PNG, JPEG, GIF, WebP, SVG, and BMP, with transparency support in formats like PNG, WebP, and SVG. The image fill system handles content scaling, positioning, and optimization automatically while giving you full programmatic control when needed. ## Checking Image Fill Support Before working with fills, verify that a block supports fill operations. Not all blocks in CE.SDK can have fills — scenes and pages typically don't, while graphic blocks, shapes, and text blocks do. ```swift highlight-fillsImage-checkSupport let canHaveFill = try engine.block.supportsFill(block) print("Block supports fills: \(canHaveFill)") ``` `engine.block.supportsFill(_:)` returns `true` when the block can have a fill assigned to it. Always check this before attempting to access fill APIs to avoid throwing on unsupported blocks. ## Creating Image Fills Create an image fill with `engine.block.createFill(.image)`, then attach it to a graphic block with `engine.block.setFill`. The fill is a separate block from the graphic — the URI lives on the fill, and the graphic renders with that image as its content. ### Manual Image Fill Creation Create the fill, set the URI on its `"fill/image/imageFileURI"` property, and assign it to the block. ```swift highlight-fillsImage-createImageFill let imageFill = try engine.block.createFill(.image) try engine.block.setURL( imageFill, property: "fill/image/imageFileURI", value: sampleImageURL, ) try engine.block.setFill(block, fill: imageFill) ``` The fill exists independently until you attach it to a block. If you create a fill but don't attach it, destroy it with `engine.block.destroy(_:)` to avoid memory leaks. When you replace an existing fill on a block by calling `setFill` again, the old fill becomes unowned and should be destroyed as well. ### Getting the Current Fill Retrieve the fill from a block with `engine.block.getFill(_:)` and inspect its type with `engine.block.getType(_:)` to verify it's an image fill. ```swift highlight-fillsImage-getCurrentFill let currentFill = try engine.block.getFill(block) let fillType = try engine.block.getType(currentFill) print("Fill type: \(fillType)") ``` `getFill` returns the fill's `DesignBlockID`, which you can then use to query the fill's type and properties. The returned type string for image fills is always `"//ly.img.ubq/fill/image"`. ## Configuring Content Fill Modes Content fill modes control how images scale and position within their containing blocks. The engine provides two primary modes — `Cover` and `Contain` — set through the `"contentFill/mode"` enum property on the block (not the fill). ### Cover Mode `Cover` mode ensures the image fills the entire block while maintaining its aspect ratio. Parts of the image may be cropped if the aspect ratios don't match, but there will never be empty space inside the block. ```swift highlight-fillsImage-coverMode try engine.block.setEnum(block, property: "contentFill/mode", value: "Cover") ``` Cover mode is ideal for backgrounds, hero images, and photo frames where you want the block completely filled with image content. The image is scaled to cover the entire area, and any overflow is cropped. ### Contain Mode `Contain` mode fits the entire image within the block while maintaining its aspect ratio. This may leave empty space if the aspect ratios don't match, but the entire image will always be visible. ```swift highlight-fillsImage-containMode try engine.block.setEnum(block, property: "contentFill/mode", value: "Contain") ``` Contain mode is best for logos, product images, and situations where preserving the complete image visibility is more important than filling the entire block. ### Getting the Current Fill Mode Query the current fill mode with `engine.block.getEnum(_:property:)` to understand how the image is being displayed. ```swift highlight-fillsImage-getFillMode let currentMode = try engine.block.getEnum(block, property: "contentFill/mode") print("Current fill mode: \(currentMode)") ``` The returned value is the string form of the enum — `"Cover"` or `"Contain"` — matching the values accepted by `setEnum`. ## Working with Source Sets Source sets enable responsive images by providing multiple resolutions of the same image. The engine automatically selects the most appropriate size based on the current display context, optimizing both performance and visual quality. ### Setting Up a Source Set A source set is an array of `Source` values, each carrying a URI and pixel dimensions. ```swift highlight-fillsImage-sourceSet try engine.block.setSourceSet( imageFill, property: "fill/image/sourceSet", sourceSet: [ Source(uri: imagesURL.appendingPathComponent("sample_1-512x341.jpg"), width: 512, height: 341), Source(uri: imagesURL.appendingPathComponent("sample_1-883x589.jpg"), width: 883, height: 589), Source(uri: imagesURL.appendingPathComponent("sample_1-1767x1178.jpg"), width: 1767, height: 1178), ], ) ``` The engine calculates the current drawing size and picks the source with the closest width that meets or exceeds the required dimensions. During export the highest available resolution is used. > **Note:** Source sets are especially useful when previewing on screens with limited > bandwidth while still exporting at full resolution. When both > `"fill/image/sourceSet"` and `"fill/image/imageFileURI"` are set on a fill, > the engine prefers the source set and ignores the single URI; the URI value > is preserved and used again as soon as the source set is cleared. ### Retrieving Source Sets Inspect the current source set on a fill with `engine.block.getSourceSet(_:property:)`. ```swift highlight-fillsImage-getSourceSet let sourceSet = try engine.block.getSourceSet(imageFill, property: "fill/image/sourceSet") print("Source set entries: \(sourceSet.count)") ``` The result is an array of `Source` instances with the same `uri`, `width`, and `height` fields you provided. ## Loading Images from Different Sources CE.SDK's image fills accept image content from several source types, giving you flexibility in how you provide content to your designs. ### Data URIs and Base64 Embed image data directly using a base64-encoded data URI. This is particularly useful for small images, icons, or dynamically generated graphics where you want to avoid a network request. ```swift highlight-fillsImage-dataUri let svgContent = """ \ \ """ let svgData = Data(svgContent.utf8).base64EncodedString() let svgDataUri = "data:image/svg+xml;base64,\(svgData)" let dataUriBlock = try engine.block.create(.graphic) try engine.block.setShape(dataUriBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(dataUriBlock, value: 120) try engine.block.setHeight(dataUriBlock, value: 120) try engine.block.setPositionX(dataUriBlock, value: 640) try engine.block.setPositionY(dataUriBlock, value: 60) try engine.block.appendChild(to: page, child: dataUriBlock) let dataUriFill = try engine.block.createFill(.image) try engine.block.setString(dataUriFill, property: "fill/image/imageFileURI", value: svgDataUri) try engine.block.setFill(dataUriBlock, fill: dataUriFill) ``` Data URIs embed the full image inside the URI string itself, eliminating network requests. This increases the scene file size, so reserve it for smaller images or cases where guaranteed availability without network dependencies matters. ## Additional Techniques ### Controlling Opacity Control the overall opacity of a block with `engine.block.setOpacity(_:value:)`. The value ranges from `0` (fully transparent) to `1` (fully opaque). ```swift highlight-fillsImage-opacity try engine.block.setOpacity(dataUriBlock, value: 0.6) ``` > **Note:** Opacity is a block property, not a fill property — it affects the entire > block, including any strokes, effects, or other visual properties applied to > the block. For transparency within the image itself, use a format that > supports alpha channels such as PNG, WebP, or SVG. ## API Reference ### Core Methods | Method | Description | |--------|-------------| | `engine.block.createFill(_:)` | Create a new fill of the given `FillType` (use `.image` for image fills) | | `engine.block.setFill(_:fill:)` | Assign a fill to a block | | `engine.block.getFill(_:)` | Get the fill block ID from a block | | `engine.block.getType(_:)` | Inspect a block's type string (e.g., `"//ly.img.ubq/fill/image"`) | | `engine.block.setURL(_:property:value:)` | Set a URL property such as the image file URI | | `engine.block.setString(_:property:value:)` | Set a string property such as a data-URI image fill | | `engine.block.setSourceSet(_:property:sourceSet:)` | Set responsive image sources | | `engine.block.getSourceSet(_:property:)` | Get the current responsive image sources | | `engine.block.setEnum(_:property:value:)` | Set an enum property such as `"contentFill/mode"` | | `engine.block.getEnum(_:property:)` | Get the current value of an enum property | | `engine.block.setOpacity(_:value:)` | Set a block's opacity from `0` to `1` | | `engine.block.supportsFill(_:)` | Check whether a block can have a fill | ### Image Fill Properties | Property | Type | Description | |----------|------|-------------| | `fill/image/imageFileURI` | `String` | Single image URI (URL or data URI) | | `fill/image/sourceSet` | `[Source]` | Array of responsive image sources with dimensions | ### Content Fill Properties | Property | Type | Values | Description | |----------|------|--------|-------------| | `contentFill/mode` | Enum | `"Cover"`, `"Contain"` | How the image scales within its block | ### Source | Property | Type | Description | |----------|------|-------------| | `uri` | `URL` | Image URI | | `width` | `UInt32` | Image width in pixels | | `height` | `UInt32` | Image height in pixels | ## Next Steps - [Fills Overview](https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/) — Understand the comprehensive fill system and all available fill types - [Color Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/color-7129cd/) — Fill blocks with solid colors - [Gradient Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/gradient-0ff079/) — Fill blocks with color transitions - [Video Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/video-ec7f9f/) — Fill blocks with video content - [Source Sets](https://img.ly/docs/cesdk/mac-catalyst/import-media/source-sets-5679c8/) — Provide multiple resolutions for responsive media - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Understand the block system that fills attach to --- ## 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: "Apply solid colors, gradients, images, or videos as fills to shapes, text, and other design elements." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/" --- > 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/) > [Fills](https://img.ly/docs/cesdk/mac-catalyst/fills-402ddc/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/) --- Fills define the visual content rendered inside the shape of a supported [design block](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/). They are separate fill objects attached to an owner block, so the owner controls geometry, layout, selection, and transforms while the fill controls the pixels drawn inside that shape. CE.SDK exposes fills through both the editor UI and the engine API, so you can manage them interactively or programmatically — and the same fill can be queried, modified, shared across blocks, or replaced at any time. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## Fill Types Choose the fill type that matches the content the block should render: | Fill type | Use it for | | ---------------- | ------------------------------------------------------------------------------------------- | | Color | Solid brand colors, simple backgrounds, masks, or placeholders. | | Linear gradient | Directional transitions between colors, such as top-to-bottom or left-to-right shading. | | Radial gradient | Circular or elliptical color transitions, such as highlights and glow effects. | | Conical gradient | Angular color transitions around a center point, such as color wheels or sweep effects. | | Image | Photos, textures, rendered thumbnails, and other still media. | | Video | Moving media inside a design block, including trimmed or looping clips. | | Pixel stream | Dynamic pixel content supplied by an integration at runtime, such as camera feeds. | Color and gradient fills are usually best for graphic shapes, backgrounds, and design accents. Image, video, and pixel stream fills are media fills: they depend on a source and often need content fitting decisions so the media appears correctly inside the block frame. ## Fill Support Not every design block can have a fill. Shapes, pages, text blocks, and many visual blocks support fills, while scene and root blocks do not. Text blocks support solid color fills only — the text color is the fill. Graphic, page, and media-capable blocks accept the broader set of fill types depending on their block capabilities. Before applying any fill operation to an arbitrary block, check fill support. A block can also have its fill disabled while retaining other styling, such as a stroke. This is useful when a shape should act as an outline, frame, or interaction target without painting its interior. ## Fill Properties Each fill type has its own properties, and they usually fall into a few categories: - Color values define the visible color of solid fills. - Gradient geometry and color stops define the direction, shape, and transitions of gradient fills. - Image sources, previews, placeholders, and source sets define how still media loads and adapts. - Video sources, playback values, trimming, looping, and related media behavior define how moving media behaves inside a block. Use the fill APIs to query and modify these properties the same way you do on any other design block. ## Content Fill Modes Media fills need a rule for fitting source content into the block frame. CE.SDK supports three content fill modes: | Mode | Behavior | | ------- | ---------------------------------------------------------------------------------------------- | | Crop | Uses explicit manual crop framing for the visible media region. | | Cover | Scales media to cover the whole block frame and may crop edges. | | Contain | Scales media so the whole source remains visible and may leave empty space. | Cover is a good default for image-heavy layouts where the frame should always be filled. Contain is better when preserving the complete source matters more than filling every pixel. Crop is best when users or templates already define the exact visible region. Cover and Contain also support horizontal and vertical alignment, so you can pull the visible region toward an edge or center. ## Ownership and Reuse A fill attached to a single block is part of that block's visual appearance — destroying the block destroys the fill with it. Duplicating the owner block duplicates an owned fill too, so changes to the duplicate can be made independently. The same fill can also be shared by multiple blocks. Shared fills are useful when several elements must stay visually synchronized, but any change to a shared fill affects every block using it. Destroying a block with a shared fill does not destroy the fill until no other blocks reference it. Use shared fills intentionally, especially when user edits should apply only to one selected block. ## Editor and Engine Workflows If on iOS, use the editor UI when users should choose or adjust fills interactively. End users pick fill types and adjust properties through the inspector — color pickers for solid colors, gradient editors for gradients, image and video pickers backed by your asset sources. Otherwise, use the CreativeEngine API to apply templates, enforce brand defaults, generate scenes programmatically, migrate existing content, or update fills from app state. The same fill model applies whether users adjust fills in the editor UI or your app updates them through the engine. ## Next Steps - [Color Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/color-7129cd/) — Work with solid color fills and their properties - [Gradient Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/gradient-0ff079/) — Apply linear, radial, and conical gradient fills - [Image Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/image-e9cb5c/) — Use images as fills for design blocks - [Video Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/video-ec7f9f/) — Use videos as fills for design blocks - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Learn how blocks define elements in a 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: "Video Fills" description: "Apply motion content to design elements by filling shapes, backgrounds, and text with videos using CE.SDK's video fill system." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/fills/video-ec7f9f/" --- > 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/) > [Fills](https://img.ly/docs/cesdk/mac-catalyst/fills-402ddc/) > [Video](https://img.ly/docs/cesdk/mac-catalyst/fills/video-ec7f9f/) --- ```swift file=@cesdk_swift_examples/engine-guides-fills-video/FillsVideo.swift reference-only import Foundation import IMGLYEngine @MainActor func fillsVideo(engine: Engine) async throws { // Demo scaffolding: a scene with a page and a graphic block to receive the video fill. 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: 500) try engine.block.setHeight(block, value: 500) try engine.block.setPositionX(block, value: 150) try engine.block.setPositionY(block, value: 50) try engine.block.appendChild(to: page, child: block) 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 canHaveFill = try engine.block.supportsFill(block) print("Block supports fills: \(canHaveFill)") let videoFill = try engine.block.createFill(.video) try engine.block.setURL( videoFill, property: "fill/video/fileURI", value: videoURL, ) try engine.block.setFill(block, fill: videoFill) let currentFill = try engine.block.getFill(block) let fillType = try engine.block.getType(currentFill) print("Fill type: \(fillType)") try engine.block.setContentFillMode(block, mode: .cover) try await engine.block.forceLoadAVResource(videoFill) // Set playback time so captures show video content rather than the black first frame. try engine.block.setPlaybackTime(page, time: 2) try await engine.captureGuide(page, label: "after-cover") try engine.block.setContentFillMode(block, mode: .contain) try await engine.captureGuide(page, label: "after-contain") try engine.block.setContentFillMode(block, mode: .crop) try engine.block.setCropScaleRatio(block, scaleRatio: 1.5) try engine.block.setCropTranslationX(block, translationX: 0.25) let currentMode = try engine.block.getContentFillMode(block) print("Current fill mode: \(currentMode)") try await engine.block.forceLoadAVResource(videoFill) let totalDuration = try engine.block.getAVResourceTotalDuration(videoFill) print("Video total duration: \(totalDuration) seconds") try engine.block.setSourceSet( videoFill, property: "fill/video/sourceSet", sourceSet: [ Source( uri: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), width: 640, height: 360, ), Source( uri: baseURL.appendingPathComponent("ly.img.video/videos/pexels-kampus-production-8154913.mp4"), width: 1280, height: 720, ), ], ) let sourceSet = try engine.block.getSourceSet(videoFill, property: "fill/video/sourceSet") print("Source set entries: \(sourceSet.count)") let ellipseBlock = try engine.block.create(.graphic) try engine.block.setShape(ellipseBlock, shape: engine.block.createShape(.ellipse)) try engine.block.setWidth(ellipseBlock, value: 200) try engine.block.setHeight(ellipseBlock, value: 200) try engine.block.setPositionX(ellipseBlock, value: 550) try engine.block.setPositionY(ellipseBlock, value: 50) try engine.block.appendChild(to: page, child: ellipseBlock) let ellipseVideoFill = try engine.block.createFill(.video) try engine.block.setURL(ellipseVideoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(ellipseBlock, fill: ellipseVideoFill) try engine.block.setOpacity(block, value: 0.7) let sharedFill = try engine.block.createFill(.video) try engine.block.setURL(sharedFill, property: "fill/video/fileURI", value: videoURL) let sharedBlock1 = try engine.block.create(.graphic) try engine.block.setShape(sharedBlock1, shape: engine.block.createShape(.rect)) try engine.block.setWidth(sharedBlock1, value: 200) try engine.block.setHeight(sharedBlock1, value: 150) try engine.block.setPositionX(sharedBlock1, value: 50) try engine.block.setPositionY(sharedBlock1, value: 400) try engine.block.appendChild(to: page, child: sharedBlock1) try engine.block.setFill(sharedBlock1, fill: sharedFill) let sharedBlock2 = try engine.block.create(.graphic) try engine.block.setShape(sharedBlock2, shape: engine.block.createShape(.rect)) try engine.block.setWidth(sharedBlock2, value: 200) try engine.block.setHeight(sharedBlock2, value: 150) try engine.block.setPositionX(sharedBlock2, value: 300) try engine.block.setPositionY(sharedBlock2, value: 400) try engine.block.appendChild(to: page, child: sharedBlock2) try engine.block.setFill(sharedBlock2, fill: sharedFill) print("Two blocks share one video fill instance") // Reset the source set back to the single video so the hero shows consistent video content. try engine.block.setSourceSet( videoFill, property: "fill/video/sourceSet", sourceSet: [Source(uri: videoURL, width: 720, height: 1280)], ) try engine.block.setContentFillMode(block, mode: .cover) try engine.block.setContentFillMode(ellipseBlock, mode: .contain) try engine.block.setOpacity(block, value: 1) try await engine.captureGuide(page, label: "hero") } ``` Fill graphic blocks with video content from URLs or asset libraries using CE.SDK's video fill system. ![Multiple blocks filled with video content — a rectangle in Cover mode, an ellipse in Contain mode, and two shared-fill rectangles — demonstrating the video fill system](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-fills-video) Understanding the distinction between **video fills** and **video blocks** is essential. Video fills are fill objects that can be applied to any block supporting fills — shapes, text, backgrounds — to paint them with video content. Video blocks are dedicated time-based blocks with full editing capabilities like trimming and duration control. Video fills focus on applying video as a visual treatment, while video blocks provide complete video editing functionality. This guide covers how to create video fills, apply them to blocks, configure fill modes, and work with video resources programmatically. ## Understanding Video Fills ### What is a Video Fill? A video fill is a fill object that paints a design block with video content. Like color and image fills, video fills are part of CE.SDK's broader fill system. Video fills are identified by the type `"//ly.img.ubq/fill/video"` or the `FillType.video` case. They contain properties for the video source, positioning, scaling, and playback behavior. ### Video Fill vs Video Blocks **Video fills** are fill objects created with `engine.block.createFill(.video)` and applied to blocks with `engine.block.setFill(_:fill:)`. You can use them to fill shapes with video content, create video backgrounds, or add video textures to text. **Video blocks** are dedicated graphic blocks with a video fill pre-configured. They come with time-based properties including trim support, duration, and playback time. Use video blocks when you need features like trimming, duration adjustment, and precise playback control. This guide focuses on video fills — applying video content as a fill to design elements. ## Checking Video Fill Support Before working with fills, verify that a block supports fill operations. Most blocks support fills — graphic blocks and text do. Scenes and cameras don't. ```swift highlight-fillsVideo-checkSupport let canHaveFill = try engine.block.supportsFill(block) print("Block supports fills: \(canHaveFill)") ``` `engine.block.supportsFill(_:)` returns `true` when the block can have a fill assigned to it. Always check this before attempting to access fill APIs to avoid throwing on unsupported blocks. ## Creating Video Fills ### Creating Video Fills Create a video fill with `engine.block.createFill(.video)`, set its source URI via the `"fill/video/fileURI"` property, and attach it to a graphic block with `engine.block.setFill`. ```swift highlight-fillsVideo-createVideoFill let videoFill = try engine.block.createFill(.video) try engine.block.setURL( videoFill, property: "fill/video/fileURI", value: videoURL, ) try engine.block.setFill(block, fill: videoFill) ``` The fill exists independently until you attach it to a block. If you create a fill but don't attach it, destroy it with `engine.block.destroy(_:)` to avoid memory leaks. When you replace an existing fill on a block by calling `setFill` again, the old fill becomes unowned and should be destroyed as well. ### Getting Current Fill Information Retrieve the fill from a block with `engine.block.getFill(_:)` and inspect its type with `engine.block.getType(_:)` to verify it's a video fill. ```swift highlight-fillsVideo-getCurrentFill let currentFill = try engine.block.getFill(block) let fillType = try engine.block.getType(currentFill) print("Fill type: \(fillType)") ``` `getFill` returns the fill's `DesignBlockID`, which you can then use to query the fill's type and properties. The returned type string for video fills is always `"//ly.img.ubq/fill/video"`. ## Content Fill Modes Content fill modes control how video scales and positions within blocks. The three modes — `Cover`, `Contain`, and `Crop` — are set via `engine.block.setContentFillMode(_:mode:)` on the block (not the fill). ### Cover Mode `Cover` mode is the default. It ensures the video fills the entire block while maintaining its aspect ratio. Parts of the video may be cropped if the aspect ratios don't match, but there will never be empty space inside the block. ```swift highlight-fillsVideo-coverMode try engine.block.setContentFillMode(block, mode: .cover) ``` Cover mode is ideal for background videos, full-frame video content, and video textures where you want the block completely filled. The video is scaled to cover the entire area, and any overflow is cropped. ### Contain Mode `Contain` mode fits the entire video within the block while maintaining its aspect ratio. This may leave empty space if the aspect ratios don't match, but the entire video will always be visible. ```swift highlight-fillsVideo-containMode try engine.block.setContentFillMode(block, mode: .contain) ``` Contain mode is best for presentations, product demos, and situations where preserving complete video visibility is more important than filling the entire block. ### Crop Mode `Crop` mode gives you full control over how the video is positioned and scaled within the block using the crop scale and translation APIs. Unlike Cover and Contain, which position content automatically, Crop is an explicit opt-in for manual positioning. ```swift highlight-fillsVideo-cropMode try engine.block.setContentFillMode(block, mode: .crop) try engine.block.setCropScaleRatio(block, scaleRatio: 1.5) try engine.block.setCropTranslationX(block, translationX: 0.25) ``` Use Crop mode when you need precise control over which portion of the video is visible — detail shots, custom compositions, or user-controlled framing. ### Getting the Current Fill Mode Query the current fill mode with `engine.block.getContentFillMode(_:)` to understand how the video is being displayed. ```swift highlight-fillsVideo-getFillMode let currentMode = try engine.block.getContentFillMode(block) print("Current fill mode: \(currentMode)") ``` The available modes are: - `.cover` — Default mode; fill entire area, may crop content - `.contain` — Show all content, may leave empty space - `.crop` — Manual positioning via crop scale and translation APIs ## Loading Video Resources Before accessing video metadata like duration, you must force load the video resource. Videos load asynchronously, and metadata is not available until the resource has been fetched. ```swift highlight-fillsVideo-forceLoad try await engine.block.forceLoadAVResource(videoFill) let totalDuration = try engine.block.getAVResourceTotalDuration(videoFill) print("Video total duration: \(totalDuration) seconds") ``` `engine.block.forceLoadAVResource(_:)` downloads the video headers and makes metadata available. Once loaded, you can read the video's total duration with `engine.block.getAVResourceTotalDuration(_:)`. Skipping this step causes the engine to throw an error when you access metadata properties — the video headers must be downloaded first. Always `await` the call before querying video-specific properties. ## Working with Source Sets Source sets enable responsive videos by providing multiple resolutions of the same video. The engine automatically selects the most appropriate size based on the current display context, optimizing both performance and visual quality. ### Setting Up a Source Set A source set is an array of `Source` values, each carrying a URI and pixel dimensions. ```swift highlight-fillsVideo-sourceSet try engine.block.setSourceSet( videoFill, property: "fill/video/sourceSet", sourceSet: [ Source( uri: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), width: 640, height: 360, ), Source( uri: baseURL.appendingPathComponent("ly.img.video/videos/pexels-kampus-production-8154913.mp4"), width: 1280, height: 720, ), ], ) ``` The engine calculates the current drawing size and picks the source with the closest dimensions that meet or exceed the required size. During export the highest available resolution is used. > **Note:** When both `"fill/video/sourceSet"` and `"fill/video/fileURI"` are set on a > fill, the engine prefers the source set and ignores the single URI; the URI > value is preserved and used again as soon as the source set is cleared. ### Retrieving Source Sets Inspect the current source set on a fill with `engine.block.getSourceSet(_:property:)`. ```swift highlight-fillsVideo-getSourceSet let sourceSet = try engine.block.getSourceSet(videoFill, property: "fill/video/sourceSet") print("Source set entries: \(sourceSet.count)") ``` The result is an array of `Source` instances with the same `uri`, `width`, and `height` fields you provided. ## Common Use Cases ### Video as Shape Fill Video fills aren't limited to rectangles. You can fill any shape with video content — the video is masked to the shape boundary. ```swift highlight-fillsVideo-shapeFill let ellipseBlock = try engine.block.create(.graphic) try engine.block.setShape(ellipseBlock, shape: engine.block.createShape(.ellipse)) try engine.block.setWidth(ellipseBlock, value: 200) try engine.block.setHeight(ellipseBlock, value: 200) try engine.block.setPositionX(ellipseBlock, value: 550) try engine.block.setPositionY(ellipseBlock, value: 50) try engine.block.appendChild(to: page, child: ellipseBlock) let ellipseVideoFill = try engine.block.createFill(.video) try engine.block.setURL(ellipseVideoFill, property: "fill/video/fileURI", value: videoURL) try engine.block.setFill(ellipseBlock, fill: ellipseVideoFill) ``` Ellipses, polygons, stars, and custom vector paths all support video fills. The video content fills the shape area, creating masked video effects. ### Video with Opacity Control the transparency of video-filled blocks to create overlay effects or blend video content with backgrounds. ```swift highlight-fillsVideo-opacity try engine.block.setOpacity(block, value: 0.7) ``` > **Note:** Opacity is a block property, not a fill property — it affects the entire > block, including any strokes, effects, or other visual properties applied to > the block. ## Additional Techniques ### Sharing Video Fills Multiple blocks can share a single video fill instance. Changes to the shared fill — such as updating the video URI — affect all blocks that use it. ```swift highlight-fillsVideo-sharedFill let sharedFill = try engine.block.createFill(.video) try engine.block.setURL(sharedFill, property: "fill/video/fileURI", value: videoURL) let sharedBlock1 = try engine.block.create(.graphic) try engine.block.setShape(sharedBlock1, shape: engine.block.createShape(.rect)) try engine.block.setWidth(sharedBlock1, value: 200) try engine.block.setHeight(sharedBlock1, value: 150) try engine.block.setPositionX(sharedBlock1, value: 50) try engine.block.setPositionY(sharedBlock1, value: 400) try engine.block.appendChild(to: page, child: sharedBlock1) try engine.block.setFill(sharedBlock1, fill: sharedFill) let sharedBlock2 = try engine.block.create(.graphic) try engine.block.setShape(sharedBlock2, shape: engine.block.createShape(.rect)) try engine.block.setWidth(sharedBlock2, value: 200) try engine.block.setHeight(sharedBlock2, value: 150) try engine.block.setPositionX(sharedBlock2, value: 300) try engine.block.setPositionY(sharedBlock2, value: 400) try engine.block.appendChild(to: page, child: sharedBlock2) try engine.block.setFill(sharedBlock2, fill: sharedFill) print("Two blocks share one video fill instance") ``` This pattern reduces memory usage when the same video appears multiple times in a composition. Shared fills play back in sync — all blocks display the same frame at the same time during playback. ## Troubleshooting ### Video Not Visible If your video fill doesn't appear, check several common causes. Verify the fill is enabled with `engine.block.isFillEnabled(_:)`. Ensure the video URL is accessible and the block has valid dimensions (width and height greater than zero) and exists in the scene hierarchy. Check that the video format is supported on your platform. MP4 with H.264 encoding works reliably across platforms, while other codecs may have limited support. ### Cannot Create Video Fill If creating a video fill throws an error, verify the block supports fills using `engine.block.supportsFill(_:)` and that the block is part of a valid scene hierarchy. ### Video Not Loading When videos fail to load, verify network connectivity for remote URLs. Validate the URI format uses `https://` for remote videos or appropriate schemes for local files. Test with a known working video URL to isolate whether the issue is with your specific video or a broader configuration problem. ### Metadata Not Available If `engine.block.getAVResourceTotalDuration(_:)` throws an error, call `engine.block.forceLoadAVResource(_:)` before accessing the property and `await` the result. The engine throws when the video headers haven't been downloaded yet. ### Memory Leaks Always destroy replaced fills to prevent memory leaks. When changing a block's fill, retrieve the old fill with `engine.block.getFill(_:)`, assign the new fill with `engine.block.setFill(_:fill:)`, then destroy the old fill with `engine.block.destroy(_:)`. Don't create fills without attaching them to blocks — unattached fills remain in memory indefinitely. Clean up shared fills when no blocks reference them anymore. ### Performance Issues Video playback is resource-intensive. Use appropriately sized videos — avoid massive files that strain decoding hardware. Consider lower resolutions for editing with high-resolution sources reserved for export. Limit the number of simultaneously playing videos, especially on mobile devices. Too many concurrent video decodes overwhelm device capabilities. Compress videos before use to reduce file sizes and improve loading times. ## API Reference ### Core Methods | Method | Description | |--------|-------------| | `engine.block.createFill(_:)` | Create a new fill of the given `FillType` (use `.video` for video fills) | | `engine.block.setFill(_:fill:)` | Assign a fill to a block | | `engine.block.getFill(_:)` | Get the fill block ID from a block | | `engine.block.getType(_:)` | Inspect a block's type string (e.g., `"//ly.img.ubq/fill/video"`) | | `engine.block.setString(_:property:value:)` | Set a string property such as the video URI | | `engine.block.getString(_:property:)` | Get the current value of a string property | | `engine.block.setContentFillMode(_:mode:)` | Set the content fill mode (`.cover`, `.contain`, or `.crop`) | | `engine.block.getContentFillMode(_:)` | Get the current `ContentFillMode` | | `engine.block.getAVResourceTotalDuration(_:)` | Get the video duration in seconds (requires `forceLoadAVResource` first) | | `engine.block.setOpacity(_:value:)` | Set a block's opacity from `0` to `1` | | `engine.block.supportsFill(_:)` | Check whether a block can have a fill | | `engine.block.setSourceSet(_:property:sourceSet:)` | Set responsive video sources | | `engine.block.getSourceSet(_:property:)` | Get the current responsive video sources | | `engine.block.isFillEnabled(_:)` | Check whether a block's fill is currently enabled | | `engine.block.forceLoadAVResource(_:)` | Force load video metadata before accessing properties | | `engine.block.generateVideoThumbnailSequence(_:thumbnailHeight:timeRange:numberOfFrames:)` | Generate a sequence of video thumbnail frames | ### Video Fill Properties | Property | Type | Description | |----------|------|-------------| | `fill/video/fileURI` | `String` | Single video URI (URL) | | `fill/video/sourceSet` | `[Source]` | Array of responsive video sources with dimensions | ### Content Fill Mode | `ContentFillMode` | Description | |----------|-------------| | `.cover` | Default. Fill entire block area, may crop content | | `.contain` | Show all content, may leave empty space | | `.crop` | Manual positioning via crop scale and translation APIs | ### Source | Property | Type | Description | |----------|------|-------------| | `uri` | `URL` | Video URI | | `width` | `UInt32` | Video width in pixels | | `height` | `UInt32` | Video height in pixels | ## Next Steps - [Fills Overview](https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/) — Comprehensive overview of the fill system - [Image Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/image-e9cb5c/) — Fill blocks with static image content - [Source Sets](https://img.ly/docs/cesdk/mac-catalyst/import-media/source-sets-5679c8/) — Provide multiple resolutions for responsive media - [Trim Video Clips](https://img.ly/docs/cesdk/mac-catalyst/edit-video/trim-4f688b/) — Trim and adjust video clip 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: "Filters and Effects" description: "Enhance visual elements with filters and effects such as blur, duotone, LUTs, and chroma keying." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/" --- > 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/) > [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/overview-299b15/) - Enhance visual elements with filters and effects such as blur, duotone, LUTs, and chroma keying. - [Supported Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/support-a666dd/) - Discover the filters and effects available in CE.SDK and check whether a block supports them using the Swift Engine API. - [Apply a Filter or Effect](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) - Programmatically apply, configure, and manage filters and effects on design elements with the CE.SDK engine. - [Create Custom Filters](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-filters-c796ba/) - Extend CE.SDK with custom LUT filter asset sources for brand-specific color grading and filter collections. - [Chroma Key (Green Screen)](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/chroma-key-green-screen-1e3e99/) - Apply the green screen effect to images and videos, replacing specific colors with transparency for compositing workflows. - [Blur Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/blur-71d642/) - Apply blur effects to design blocks to soften backgrounds, create depth, or draw attention with the CE.SDK engine. - [Create a Custom LUT Filter](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-lut-filter-6e3f49/) - Create and apply custom LUT filters to achieve consistent, brand-aligned visual styles. - [Distortion Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/distortion-5b5a66/) - Apply distortion effects to warp, shift, and transform design elements for dynamic artistic visuals in CE.SDK. - [Duotone](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/duotone-831fc5/) - Apply duotone effects to images with the CE.SDK Engine, mapping tones to two colors for stylized, brand-consistent visuals. --- ## 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 a Filter or Effect" description: "Programmatically apply, configure, and manage filters and effects on design elements with the CE.SDK engine." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/" --- > 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/) > [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) > [Apply Filter or Effect](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) --- ```swift file=@cesdk_swift_examples/engine-guides-using-effects/UsingEffects.swift reference-only import Foundation import IMGLYEngine @MainActor func usingEffects(engine: Engine) async throws { // Demo scaffolding: a scene with one page laid out as a 2x3 comparison grid. // Each cell is a graphic block with an image fill of the same sample image, // so the result shows the same photo under different effects side by side. 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 let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") let originalCell = try makeImageCell(engine: engine, page: page, x: 20, y: 20, imageURL: imageURL) let pixelizeCell = try makeImageCell(engine: engine, page: page, x: 280, y: 20, imageURL: imageURL) let adjustmentsCell = try makeImageCell(engine: engine, page: page, x: 540, y: 20, imageURL: imageURL) let duotoneCell = try makeImageCell(engine: engine, page: page, x: 20, y: 300, imageURL: imageURL) let lutCell = try makeImageCell(engine: engine, page: page, x: 280, y: 300, imageURL: imageURL) let combinedCell = try makeImageCell(engine: engine, page: page, x: 540, y: 300, imageURL: imageURL) try await engine.captureGuide(page, label: "after-image") let sceneSupportsEffects = try engine.block.supportsEffects(scene) // false let blockSupportsEffects = try engine.block.supportsEffects(originalCell) // true print("scene supports effects: \(sceneSupportsEffects)") print("graphic block supports effects: \(blockSupportsEffects)") let pixelize = try engine.block.createEffect(.pixelize) let adjustments = try engine.block.createEffect(.adjustments) try engine.block.appendEffect(pixelizeCell, effectID: pixelize) try engine.block.appendEffect(adjustmentsCell, effectID: adjustments) let pixelizeProperties = try engine.block.findAllProperties(pixelize) let adjustmentProperties = try engine.block.findAllProperties(adjustments) print("pixelize properties: \(pixelizeProperties)") print("adjustment properties: \(adjustmentProperties)") try engine.block.setInt(pixelize, property: "effect/pixelize/horizontalPixelSize", value: 10) try engine.block.setFloat(adjustments, property: "effect/adjustments/brightness", value: 0.2) try engine.block.setFloat(adjustments, property: "effect/adjustments/contrast", value: 0.15) let lutFilter = try engine.block.createEffect(.lutFilter) try engine.block.setURL( lutFilter, property: "effect/lut_filter/lutFileURI", value: baseURL.appendingPathComponent("ly.img.filter.lut/LUTs/imgly_lut_ad1920_5_5_128.png"), ) try engine.block.setInt(lutFilter, property: "effect/lut_filter/horizontalTileCount", value: 5) try engine.block.setInt(lutFilter, property: "effect/lut_filter/verticalTileCount", value: 5) try engine.block.setFloat(lutFilter, property: "effect/lut_filter/intensity", value: 0.9) try engine.block.appendEffect(lutCell, effectID: lutFilter) let duotone = try engine.block.createEffect(.duotoneFilter) try engine.block.setColor( duotone, property: "effect/duotone_filter/darkColor", color: .rgba(r: 0.1, g: 0.2, b: 0.4, a: 1), ) try engine.block.setColor( duotone, property: "effect/duotone_filter/lightColor", color: .rgba(r: 0.95, g: 0.85, b: 0.6, a: 1), ) try engine.block.setFloat(duotone, property: "effect/duotone_filter/intensity", value: 0.8) try engine.block.appendEffect(duotoneCell, effectID: duotone) try await engine.captureGuide(page, label: "after-effects") let comboAdjustments = try engine.block.createEffect(.adjustments) try engine.block.setFloat(comboAdjustments, property: "effect/adjustments/brightness", value: 0.2) let comboDuotone = try engine.block.createEffect(.duotoneFilter) try engine.block.setColor( comboDuotone, property: "effect/duotone_filter/lightColor", color: .rgba(r: 0.95, g: 0.85, b: 0.6, a: 1), ) try engine.block.setFloat(comboDuotone, property: "effect/duotone_filter/intensity", value: 0.6) let comboPixelize = try engine.block.createEffect(.pixelize) try engine.block.setInt(comboPixelize, property: "effect/pixelize/horizontalPixelSize", value: 6) try engine.block.appendEffect(combinedCell, effectID: comboDuotone) try engine.block.appendEffect(combinedCell, effectID: comboPixelize) try engine.block.insertEffect(combinedCell, effectID: comboAdjustments, index: 0) let effectsList = try engine.block.getEffects(combinedCell) print("applied effects: \(effectsList)") try await engine.captureGuide(page, label: "hero") try engine.block.setEffectEnabled(effectID: comboPixelize, enabled: false) let pixelizeEnabled = try engine.block.isEffectEnabled(effectID: comboPixelize) print("pixelize enabled: \(pixelizeEnabled)") try engine.block.removeEffect(combinedCell, index: 2) try engine.block.destroy(comboPixelize) let graphicBlocks = try engine.block.find(byType: .graphic) for graphic in graphicBlocks { guard try engine.block.supportsEffects(graphic) else { continue } let batchAdjustments = try engine.block.createEffect(.adjustments) try engine.block.setFloat(batchAdjustments, property: "effect/adjustments/brightness", value: 0.1) try engine.block.appendEffect(graphic, effectID: batchAdjustments) } try applyVintagePreset(engine: engine, to: originalCell) } @MainActor private func makeImageCell( engine: Engine, page: DesignBlockID, x: Float, y: Float, imageURL: URL, ) throws -> DesignBlockID { let cell = try engine.block.create(.graphic) try engine.block.setShape(cell, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(cell, value: x) try engine.block.setPositionY(cell, value: y) try engine.block.setWidth(cell, value: 240) try engine.block.setHeight(cell, value: 260) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(cell, fill: fill) try engine.block.appendChild(to: page, child: cell) return cell } @MainActor private func applyVintagePreset(engine: Engine, to block: DesignBlockID) throws { let adjustments = try engine.block.createEffect(.adjustments) try engine.block.setFloat(adjustments, property: "effect/adjustments/contrast", value: -0.15) try engine.block.setFloat(adjustments, property: "effect/adjustments/saturation", value: -0.2) try engine.block.appendEffect(block, effectID: adjustments) let duotone = try engine.block.createEffect(.duotoneFilter) try engine.block.setColor( duotone, property: "effect/duotone_filter/darkColor", color: .rgba(r: 0.2, g: 0.15, b: 0.1, a: 1), ) try engine.block.setColor( duotone, property: "effect/duotone_filter/lightColor", color: .rgba(r: 0.95, g: 0.9, b: 0.75, a: 1), ) try engine.block.setFloat(duotone, property: "effect/duotone_filter/intensity", value: 0.4) try engine.block.appendEffect(block, effectID: duotone) } ``` Apply color grading, blur, pixelization, and other visual treatments to design elements using CE.SDK's effect system, then configure, stack, and manage them with the engine API. ![The same photo shown in a grid under different effects: the original, pixelization, an adjustments effect, a duotone filter, a LUT filter, and a combination of effects](./assets/swift-based.hero.webp) > **Reading time:** 9 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-using-effects) CE.SDK uses a single effect API for both filters and effects. **Filters** apply color transformations such as LUT filters and duotone, while **effects** apply visual modifications such as blur, pixelize, vignette, and image adjustments. Both are created with `createEffect` and attached to a block's effect list, where they render in order and can be stacked, toggled, and removed individually. Many design blocks — such as pages and graphic blocks — support effects, while others like the scene block do not. The examples below apply effects to graphic blocks with image fills, laying out the same photo in a grid so each effect can be compared against the original. ## Programmatic Effect Application Apply, configure, and combine effects directly through the engine's block API. ### Check Effect Support Not every block type accepts effects, so call `supportsEffects(_:)` before reaching for any of the other effect APIs. It returns `false` for the scene block and `true` for a graphic block. ```swift highlight-usingEffects-supportsEffects let sceneSupportsEffects = try engine.block.supportsEffects(scene) // false let blockSupportsEffects = try engine.block.supportsEffects(originalCell) // true print("scene supports effects: \(sceneSupportsEffects)") print("graphic block supports effects: \(blockSupportsEffects)") ``` ### Create an Effect Create a new effect instance with `createEffect(_:)`, passing the `EffectType` you want. Here we create a pixelize effect and an adjustments effect. Creating an effect does not change the rendered output yet — the effect has to be attached to a block first. ```swift highlight-usingEffects-createEffect let pixelize = try engine.block.createEffect(.pixelize) let adjustments = try engine.block.createEffect(.adjustments) ``` ### Add Effects to a Block Attach an effect to the end of a block's effect list with `appendEffect(_:effectID:)`, or place it at a specific position with `insertEffect(_:effectID:index:)`. Use `removeEffect(_:index:)` to take one out of the list. Here the pixelize and adjustments effects are each appended to their own image block. ```swift highlight-usingEffects-addEffect try engine.block.appendEffect(pixelizeCell, effectID: pixelize) try engine.block.appendEffect(adjustmentsCell, effectID: adjustments) ``` ### Configure Effect Properties Each effect type exposes its own set of properties. List them with `findAllProperties(_:)`, then set each value with the typed setter that matches the property — for example `setFloat(_:property:value:)` for amounts like brightness and contrast, and `setInt(_:property:value:)` for discrete values like pixel size. ```swift highlight-usingEffects-getProperties let pixelizeProperties = try engine.block.findAllProperties(pixelize) let adjustmentProperties = try engine.block.findAllProperties(adjustments) print("pixelize properties: \(pixelizeProperties)") print("adjustment properties: \(adjustmentProperties)") ``` The adjustments effect leaves the image unchanged until at least one of its properties — such as `brightness` — is set to a non-zero value. ```swift highlight-usingEffects-modifyProperties try engine.block.setInt(pixelize, property: "effect/pixelize/horizontalPixelSize", value: 10) try engine.block.setFloat(adjustments, property: "effect/adjustments/brightness", value: 0.2) try engine.block.setFloat(adjustments, property: "effect/adjustments/contrast", value: 0.15) ``` ### Apply LUT Filters A LUT (look-up table) filter applies color grading by mapping each source color through a lookup-table image. Set the LUT image with `setURL(_:property:value:)` on `effect/lut_filter/lutFileURI`, match `horizontalTileCount` and `verticalTileCount` to the grid baked into that image, and control the blend with `intensity` (from `0.0` for the original to `1.0` for the full filter). ```swift highlight-usingEffects-lutFilter let lutFilter = try engine.block.createEffect(.lutFilter) try engine.block.setURL( lutFilter, property: "effect/lut_filter/lutFileURI", value: baseURL.appendingPathComponent("ly.img.filter.lut/LUTs/imgly_lut_ad1920_5_5_128.png"), ) try engine.block.setInt(lutFilter, property: "effect/lut_filter/horizontalTileCount", value: 5) try engine.block.setInt(lutFilter, property: "effect/lut_filter/verticalTileCount", value: 5) try engine.block.setFloat(lutFilter, property: "effect/lut_filter/intensity", value: 0.9) try engine.block.appendEffect(lutCell, effectID: lutFilter) ``` In a full editor you can also browse the available LUT filters through the asset library with `engine.asset.findAssets(...)` and apply them by their metadata. See [Create a Custom LUT Filter](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-lut-filter-6e3f49/) for the complete LUT workflow, including the tile-count layout. ### Apply Duotone Filters A duotone filter remaps the image's tones onto two colors, mapping darker areas toward a dark color and lighter areas toward a light color. Set both colors with `setColor(_:property:color:)`. The `intensity` property is a mixing weight from `-1.0` to `1.0`: positive values emphasize the light color and negative values emphasize the dark color. ```swift highlight-usingEffects-duotoneFilter let duotone = try engine.block.createEffect(.duotoneFilter) try engine.block.setColor( duotone, property: "effect/duotone_filter/darkColor", color: .rgba(r: 0.1, g: 0.2, b: 0.4, a: 1), ) try engine.block.setColor( duotone, property: "effect/duotone_filter/lightColor", color: .rgba(r: 0.95, g: 0.85, b: 0.6, a: 1), ) try engine.block.setFloat(duotone, property: "effect/duotone_filter/intensity", value: 0.8) try engine.block.appendEffect(duotoneCell, effectID: duotone) ``` ### Combine Multiple Effects Stack several effects on a single block to build a layered treatment. Effects render in the order they appear in the list, so the insertion position matters. Here the adjustments effect is inserted at index `0` so it runs before the duotone and pixelize effects. ```swift highlight-usingEffects-combineEffects let comboAdjustments = try engine.block.createEffect(.adjustments) try engine.block.setFloat(comboAdjustments, property: "effect/adjustments/brightness", value: 0.2) let comboDuotone = try engine.block.createEffect(.duotoneFilter) try engine.block.setColor( comboDuotone, property: "effect/duotone_filter/lightColor", color: .rgba(r: 0.95, g: 0.85, b: 0.6, a: 1), ) try engine.block.setFloat(comboDuotone, property: "effect/duotone_filter/intensity", value: 0.6) let comboPixelize = try engine.block.createEffect(.pixelize) try engine.block.setInt(comboPixelize, property: "effect/pixelize/horizontalPixelSize", value: 6) try engine.block.appendEffect(combinedCell, effectID: comboDuotone) try engine.block.appendEffect(combinedCell, effectID: comboPixelize) try engine.block.insertEffect(combinedCell, effectID: comboAdjustments, index: 0) ``` ## Managing Applied Effects Inspect, toggle, and remove the effects already attached to a block. ### Query Applied Effects Use `getEffects(_:)` to read a block's ordered list of effect ids. This is the entry point for building an effect-management interface or inspecting what is currently applied; the order reflects how the effects render. ```swift highlight-usingEffects-getEffects let effectsList = try engine.block.getEffects(combinedCell) print("applied effects: \(effectsList)") ``` ### Enable and Disable Effects Toggle an individual effect without removing it using `setEffectEnabled(effectID:enabled:)`. Disabled effects are skipped when the block renders, while their properties stay intact for when you enable them again. Read the current state with `isEffectEnabled(effectID:)`. ```swift highlight-usingEffects-disableEffect try engine.block.setEffectEnabled(effectID: comboPixelize, enabled: false) let pixelizeEnabled = try engine.block.isEffectEnabled(effectID: comboPixelize) print("pixelize enabled: \(pixelizeEnabled)") ``` ### Destroy Unused Effects Removing an effect from a block's list does not free it. Destroy an effect you no longer need with `destroy(_:)`, the same API used for design blocks. Effects still attached to a block are destroyed automatically when the block itself is destroyed. ```swift highlight-usingEffects-destroyEffect try engine.block.removeEffect(combinedCell, index: 2) try engine.block.destroy(comboPixelize) ``` ## Additional Techniques Patterns for applying effects at scale and packaging them for reuse. ### Batch Processing Apply the same effect across many blocks by iterating over them. Check `supportsEffects(_:)` on each block before creating an effect so unsupported blocks are skipped. Here every graphic block on the page receives a small brightness lift. ```swift highlight-usingEffects-batchProcessing let graphicBlocks = try engine.block.find(byType: .graphic) for graphic in graphicBlocks { guard try engine.block.supportsEffects(graphic) else { continue } let batchAdjustments = try engine.block.createEffect(.adjustments) try engine.block.setFloat(batchAdjustments, property: "effect/adjustments/brightness", value: 0.1) try engine.block.appendEffect(graphic, effectID: batchAdjustments) } ``` ### Reusable Effect Presets Bundle a set of effects into a reusable function to apply a consistent look — a brand filter or a film emulation — across blocks or sessions. The preset below stacks a desaturating adjustments effect with a warm duotone. ```swift highlight-usingEffects-presets @MainActor private func applyVintagePreset(engine: Engine, to block: DesignBlockID) throws { let adjustments = try engine.block.createEffect(.adjustments) try engine.block.setFloat(adjustments, property: "effect/adjustments/contrast", value: -0.15) try engine.block.setFloat(adjustments, property: "effect/adjustments/saturation", value: -0.2) try engine.block.appendEffect(block, effectID: adjustments) let duotone = try engine.block.createEffect(.duotoneFilter) try engine.block.setColor( duotone, property: "effect/duotone_filter/darkColor", color: .rgba(r: 0.2, g: 0.15, b: 0.1, a: 1), ) try engine.block.setColor( duotone, property: "effect/duotone_filter/lightColor", color: .rgba(r: 0.95, g: 0.9, b: 0.75, a: 1), ) try engine.block.setFloat(duotone, property: "effect/duotone_filter/intensity", value: 0.4) try engine.block.appendEffect(block, effectID: duotone) } ``` Effect properties are fixed values rather than keyframes. To vary an effect over time, animate the block itself with the engine's animation APIs rather than mutating effect properties on a timer. ## Performance Considerations Effects render on the GPU, but each one adds work to every frame the block appears in. A few guidelines keep rendering responsive: - Stack only the effects you need — every entry in a block's list is evaluated on each render. - Color adjustments are inexpensive, while LUT filters and blurs are heavier. Favor adjustments when either would achieve the look. - On lower-powered devices, keep the number of simultaneous effects per block small. - When applying the same treatment across many blocks, reuse the same property values rather than recomputing them per block, and disable effects with `setEffectEnabled(effectID:enabled:)` during heavy editing instead of removing and recreating them. ## Troubleshooting | Symptom | Cause | Solution | | --- | --- | --- | | Effect has no visible result | The effect was created but never attached | Call `appendEffect` or `insertEffect` to add it to the block's list | | Adjustments effect does nothing | All adjustment properties are still at their default of zero | Set a property such as `effect/adjustments/brightness` to a non-zero value | | LUT filter has no visible result | The LUT image URL does not resolve, or the tile counts do not match the image | Confirm the URL loads and set `horizontalTileCount` and `verticalTileCount` to match the LUT image's grid | | Effect still renders after removal | `removeEffect` detaches but does not destroy | Call `destroy` on the effect to free it | | Effect missing after saving and reloading a scene | Effects serialize with the scene, but a referenced resource such as a LUT image may no longer resolve, or the effect was destroyed before saving | Keep the `lutFileURI` reachable after load, and avoid destroying effects you still need | | Attaching an effect throws an error | The target block does not support effects | Verify with `supportsEffects` before attaching | ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.supportsEffects(_:)` | Check whether a block supports effects | | `engine.block.createEffect(_:)` | Create a new effect instance of an `EffectType` | | `engine.block.appendEffect(_:effectID:)` | Add an effect to the end of a block's effect list | | `engine.block.insertEffect(_:effectID:index:)` | Insert an effect at a specific position | | `engine.block.removeEffect(_:index:)` | Remove the effect at an index from a block's list | | `engine.block.getEffects(_:)` | Get the ordered list of effect ids applied to a block | | `engine.block.findAllProperties(_:)` | List all property keys of an effect | | `engine.block.find(byType:)` | Find all blocks of a `DesignBlockType` | | `engine.block.setInt(_:property:value:)` | Set an integer effect property | | `engine.block.setFloat(_:property:value:)` | Set a floating-point effect property | | `engine.block.setURL(_:property:value:)` | Set a URL effect property such as a LUT file | | `engine.block.setColor(_:property:color:)` | Set a color effect property | | `engine.block.setEffectEnabled(effectID:enabled:)` | Enable or disable an effect | | `engine.block.isEffectEnabled(effectID:)` | Check whether an effect is enabled | | `engine.block.destroy(_:)` | Destroy an effect instance | ### Properties | Property | Type | Description | | --- | --- | --- | | `effect/pixelize/horizontalPixelSize` | Int | Size of the pixelization blocks | | `effect/adjustments/brightness` | Float | Brightness adjustment | | `effect/adjustments/contrast` | Float | Contrast adjustment | | `effect/lut_filter/lutFileURI` | URL | URL of the LUT image | | `effect/lut_filter/horizontalTileCount` | Int | Number of LUT tiles per row | | `effect/lut_filter/verticalTileCount` | Int | Number of LUT tiles per column | | `effect/lut_filter/intensity` | Float | LUT blend amount, from `0.0` to `1.0` | | `effect/duotone_filter/darkColor` | Color | Color mapped to shadows | | `effect/duotone_filter/lightColor` | Color | Color mapped to highlights | | `effect/duotone_filter/intensity` | Float | Mixing weight from `-1.0` to `1.0`; positive emphasizes the light color, negative the dark | ## Next Steps - [Filters & Effects Overview](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/overview-299b15/) — Browse every filter and effect CE.SDK provides. - [Create a Custom LUT Filter](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-lut-filter-6e3f49/) — Apply professional color grading with a LUT file. - [Blur Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/blur-71d642/) — Soften backgrounds and create depth with the blur API. - [Chroma Key (Green Screen)](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/chroma-key-green-screen-1e3e99/) — Remove a background color from an image or video. --- ## 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: "Blur Effects" description: "Apply blur effects to design blocks to soften backgrounds, create depth, or draw attention with the CE.SDK engine." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/blur-71d642/" --- > 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/) > [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) > [Apply Blur](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/blur-71d642/) --- ```swift file=@cesdk_swift_examples/engine-guides-blur/Blur.swift reference-only import Foundation import IMGLYEngine @MainActor func blur(engine: Engine) async throws { // Demo scaffolding: a 2x2 grid of the same photo so each cell can render // a different blur type side by side. 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 let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") let uniformCell = try makeImageCell(engine: engine, page: page, x: 20, y: 20, imageURL: imageURL) let linearCell = try makeImageCell(engine: engine, page: page, x: 400, y: 20, imageURL: imageURL) let radialCell = try makeImageCell(engine: engine, page: page, x: 20, y: 300, imageURL: imageURL) let mirroredCell = try makeImageCell(engine: engine, page: page, x: 400, y: 300, imageURL: imageURL) try await engine.captureGuide(page, label: "before-blur") guard try engine.block.supportsBlur(uniformCell) else { return } let uniformBlur = try engine.block.createBlur(.uniform) try engine.block.setBlur(uniformCell, blurID: uniformBlur) try engine.block.setBlurEnabled(uniformCell, enabled: true) try engine.block.setFloat(uniformBlur, property: "blur/uniform/intensity", value: 0.8) let linearBlur = try engine.block.createBlur(.linear) try engine.block.setFloat(linearBlur, property: "blur/linear/blurRadius", value: 35) try engine.block.setFloat(linearBlur, property: "blur/linear/x1", value: 0.0) try engine.block.setFloat(linearBlur, property: "blur/linear/y1", value: 0.3) try engine.block.setFloat(linearBlur, property: "blur/linear/x2", value: 1.0) try engine.block.setFloat(linearBlur, property: "blur/linear/y2", value: 0.7) try engine.block.setBlur(linearCell, blurID: linearBlur) try engine.block.setBlurEnabled(linearCell, enabled: true) let radialBlur = try engine.block.createBlur(.radial) try engine.block.setFloat(radialBlur, property: "blur/radial/blurRadius", value: 45) try engine.block.setFloat(radialBlur, property: "blur/radial/radius", value: 40) try engine.block.setFloat(radialBlur, property: "blur/radial/gradientRadius", value: 30) try engine.block.setFloat(radialBlur, property: "blur/radial/x", value: 0.5) try engine.block.setFloat(radialBlur, property: "blur/radial/y", value: 0.5) try engine.block.setBlur(radialCell, blurID: radialBlur) try engine.block.setBlurEnabled(radialCell, enabled: true) let mirroredBlur = try engine.block.createBlur(.mirrored) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/blurRadius", value: 50) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/size", value: 30) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/gradientSize", value: 25) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/x1", value: 0.0) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/y1", value: 0.5) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/x2", value: 1.0) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/y2", value: 0.5) try engine.block.setBlur(mirroredCell, blurID: mirroredBlur) try engine.block.setBlurEnabled(mirroredCell, enabled: true) try await engine.captureGuide(page, label: "hero") let currentBlur = try engine.block.getBlur(radialCell) let currentRadius = try engine.block.getFloat(currentBlur, property: "blur/radial/blurRadius") print("current radial blur radius: \(currentRadius)") try engine.block.setBlurEnabled(uniformCell, enabled: false) let uniformEnabled = try engine.block.isBlurEnabled(uniformCell) print("uniform blur enabled: \(uniformEnabled)") try await engine.captureGuide(page, label: "after-toggle") let sharedBlur = try engine.block.createBlur(.uniform) try engine.block.setFloat(sharedBlur, property: "blur/uniform/intensity", value: 0.4) try engine.block.setBlur(uniformCell, blurID: sharedBlur) try engine.block.setBlurEnabled(uniformCell, enabled: true) try engine.block.setBlur(linearCell, blurID: sharedBlur) try engine.block.setBlurEnabled(linearCell, enabled: true) let existingBlur = try engine.block.getBlur(mirroredCell) try engine.block.destroy(existingBlur) } @MainActor private func makeImageCell( engine: Engine, page: DesignBlockID, x: Float, y: Float, imageURL: URL, ) throws -> DesignBlockID { let cell = try engine.block.create(.graphic) try engine.block.setShape(cell, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(cell, value: x) try engine.block.setPositionY(cell, value: y) try engine.block.setWidth(cell, value: 380) try engine.block.setHeight(cell, value: 260) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(cell, fill: fill) try engine.block.appendChild(to: page, child: cell) return cell } ``` Apply blur effects to design blocks using CE.SDK's dedicated blur system for softening backgrounds, simulating depth of field, or drawing focus toward specific elements. ![The same photo shown in a 2x2 grid under different blur types: uniform, linear, radial, and mirrored](./assets/swift-based.hero.webp) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-blur) Unlike stackable effects, blur is a dedicated feature with its own API. Each block supports **exactly one** blur at a time, though the same blur instance can be shared across multiple blocks. CE.SDK provides four blur types: **uniform** for even softening, **linear** and **mirrored** for gradient-based effects along an axis, and **radial** for circular focal points. ## Programmatic Blur Application Apply, configure, and combine blur directly through the engine's block API. ### Check Blur Support Not every block type accepts blur, so call `supportsBlur(_:)` before reaching for any of the other blur APIs. ```swift highlight-blur-supportsBlur guard try engine.block.supportsBlur(uniformCell) else { return } ``` ### Create and Apply Blur Create a blur instance with `createBlur(_:)`, passing the `BlurType` you want. Attach it to a block with `setBlur(_:blurID:)` and turn it on with `setBlurEnabled(_:enabled:)`. Creating a blur does not change the rendered output — the blur has to be attached and enabled first. ```swift highlight-blur-createAndApply let uniformBlur = try engine.block.createBlur(.uniform) try engine.block.setBlur(uniformCell, blurID: uniformBlur) try engine.block.setBlurEnabled(uniformCell, enabled: true) ``` `BlurType` has four cases: - `.uniform` — `//ly.img.ubq/blur/uniform` - `.linear` — `//ly.img.ubq/blur/linear` - `.mirrored` — `//ly.img.ubq/blur/mirrored` - `.radial` — `//ly.img.ubq/blur/radial` ### Configure Blur Parameters Each blur type exposes its own set of properties. Configure them with `setFloat(_:property:value:)`. Coordinate properties such as `x`, `y`, `x1`, `y1` are relative values in the range `0.0` – `1.0`, where `(0, 0)` is the top-left of the block and `(1, 1)` is the bottom-right. ### Apply Uniform Blur The uniform blur (also known as a Gaussian blur) applies consistent softening across the entire block. It has a single parameter, `blur/uniform/intensity`, ranging from `0.0` (no blur) to `1.0` (maximum softness). ```swift highlight-blur-uniform try engine.block.setFloat(uniformBlur, property: "blur/uniform/intensity", value: 0.8) ``` ### Apply Linear Blur The linear blur creates a directional blur along a line defined by two control points. Moving the control points rotates the blur axis and shifts where the transition occurs. ```swift highlight-blur-linear let linearBlur = try engine.block.createBlur(.linear) try engine.block.setFloat(linearBlur, property: "blur/linear/blurRadius", value: 35) try engine.block.setFloat(linearBlur, property: "blur/linear/x1", value: 0.0) try engine.block.setFloat(linearBlur, property: "blur/linear/y1", value: 0.3) try engine.block.setFloat(linearBlur, property: "blur/linear/x2", value: 1.0) try engine.block.setFloat(linearBlur, property: "blur/linear/y2", value: 0.7) try engine.block.setBlur(linearCell, blurID: linearBlur) try engine.block.setBlurEnabled(linearCell, enabled: true) ``` ### Apply Radial Blur The radial blur radiates outward from a center point, keeping a circular inner area sharp. Adjust the sharp region's size with `radius` and the width of the transition band with `gradientRadius`. ```swift highlight-blur-radial let radialBlur = try engine.block.createBlur(.radial) try engine.block.setFloat(radialBlur, property: "blur/radial/blurRadius", value: 45) try engine.block.setFloat(radialBlur, property: "blur/radial/radius", value: 40) try engine.block.setFloat(radialBlur, property: "blur/radial/gradientRadius", value: 30) try engine.block.setFloat(radialBlur, property: "blur/radial/x", value: 0.5) try engine.block.setFloat(radialBlur, property: "blur/radial/y", value: 0.5) try engine.block.setBlur(radialCell, blurID: radialBlur) try engine.block.setBlurEnabled(radialCell, enabled: true) ``` ### Apply Mirrored Blur The mirrored blur creates a band of focus with blur on both sides — a tilt-shift style effect. `size` controls the width of the clear band and `gradientSize` controls how quickly the blur ramps up on either side. ```swift highlight-blur-mirrored let mirroredBlur = try engine.block.createBlur(.mirrored) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/blurRadius", value: 50) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/size", value: 30) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/gradientSize", value: 25) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/x1", value: 0.0) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/y1", value: 0.5) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/x2", value: 1.0) try engine.block.setFloat(mirroredBlur, property: "blur/mirrored/y2", value: 0.5) try engine.block.setBlur(mirroredCell, blurID: mirroredBlur) try engine.block.setBlurEnabled(mirroredCell, enabled: true) ``` ## Managing Blur Inspect the blur already attached to a block, toggle it, share it across blocks, or remove it. ### Read an Applied Blur Retrieve the blur attached to a block with `getBlur(_:)`, then read or modify its properties with the same setters and `getFloat(_:property:)`. ```swift highlight-blur-readBlur let currentBlur = try engine.block.getBlur(radialCell) let currentRadius = try engine.block.getFloat(currentBlur, property: "blur/radial/blurRadius") print("current radial blur radius: \(currentRadius)") ``` ### Enable and Disable Blur Toggle blur on and off without removing it using `setBlurEnabled(_:enabled:)`. When disabled the blur stays attached to the block and its parameters are preserved for when it is enabled again. Read the current state with `isBlurEnabled(_:)`. ```swift highlight-blur-toggle try engine.block.setBlurEnabled(uniformCell, enabled: false) let uniformEnabled = try engine.block.isBlurEnabled(uniformCell) print("uniform blur enabled: \(uniformEnabled)") ``` ### Share a Blur Across Blocks A single blur instance can be attached to multiple blocks. Create the blur once, then call `setBlur(_:blurID:)` on each block. Changes to the blur's properties update every block that uses it. ```swift highlight-blur-share let sharedBlur = try engine.block.createBlur(.uniform) try engine.block.setFloat(sharedBlur, property: "blur/uniform/intensity", value: 0.4) try engine.block.setBlur(uniformCell, blurID: sharedBlur) try engine.block.setBlurEnabled(uniformCell, enabled: true) try engine.block.setBlur(linearCell, blurID: sharedBlur) try engine.block.setBlurEnabled(linearCell, enabled: true) ``` Attaching a new blur to a block replaces the previous one on that block — a block only ever has one active blur. ### Remove a Blur To remove a blur permanently, call `destroy(_:)` on the blur block. This frees the blur and detaches it from **every block that was using it**. When you only want to turn a blur off temporarily, prefer `setBlurEnabled(_:enabled:)` instead. ```swift highlight-blur-destroy let existingBlur = try engine.block.getBlur(mirroredCell) try engine.block.destroy(existingBlur) ``` ## Troubleshooting | Symptom | Cause | Solution | | --- | --- | --- | | No blur appears | The block doesn't support blur or blur isn't enabled | Verify with `supportsBlur(_:)` and `isBlurEnabled(_:)` | | Property changes have no effect | Wrong property key | Check the exact property name — keys begin with `blur//` | | Blur looks off-center | Coordinate values outside `0.0` – `1.0` | Confirm each `x`, `y`, `x1`, `y1` value is within range | | Blur too subtle or too strong | Radius or intensity values | Increase or decrease `blurRadius` (linear, mirrored, radial) or `intensity` (uniform) | ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.supportsBlur(_:)` | Check whether a block supports blur | | `engine.block.createBlur(_:)` | Create a new blur instance of a `BlurType` | | `engine.block.setBlur(_:blurID:)` | Attach a blur to a block | | `engine.block.getBlur(_:)` | Get the blur attached to a block | | `engine.block.setBlurEnabled(_:enabled:)` | Enable or disable the blur on a block | | `engine.block.isBlurEnabled(_:)` | Check whether the blur on a block is enabled | | `engine.block.setFloat(_:property:value:)` | Set a blur property | | `engine.block.getFloat(_:property:)` | Read a blur property | | `engine.block.getType(_:)` | Get the type identifier of a blur block | | `engine.block.destroy(_:)` | Destroy a blur, detaching it from every block that used it | ### Properties | Property | Type | Default | Description | | --- | --- | --- | --- | | `blur/uniform/intensity` | Float | `0.5` | Uniform blur strength, `0.0` – `1.0` | | `blur/linear/blurRadius` | Float | `30` | Linear blur intensity | | `blur/linear/x1`, `y1` | Float | `0`, `0.5` | Linear blur start point | | `blur/linear/x2`, `y2` | Float | `1`, `0.5` | Linear blur end point | | `blur/mirrored/blurRadius` | Float | `30` | Mirrored blur intensity | | `blur/mirrored/size` | Float | `75` | Width of the unblurred band | | `blur/mirrored/gradientSize` | Float | `50` | Width of the transition zones | | `blur/mirrored/x1`, `y1`, `x2`, `y2` | Float | `0`, `0.5`, `1`, `0.5` | Mirrored blur axis points | | `blur/radial/blurRadius` | Float | `30` | Radial blur intensity | | `blur/radial/radius` | Float | `75` | Size of the sharp center | | `blur/radial/gradientRadius` | Float | `50` | Width of the transition band | | `blur/radial/x`, `y` | Float | `0.5`, `0.5` | Radial blur center point | ## Next Steps - [Apply Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) — Stack visual effects such as adjustments, LUT filters, and duotone alongside blur. - [Distortion Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/distortion-5b5a66/) — Apply warping and glitch effects to design blocks. - [Filters & Effects Overview](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/overview-299b15/) — Browse every filter and effect CE.SDK provides. - [Modify Properties](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Understand block properties and how to modify 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: "Chroma Key (Green Screen)" description: "Apply the green screen effect to images and videos, replacing specific colors with transparency for compositing workflows." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/chroma-key-green-screen-1e3e99/" --- > 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/) > [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) > [Apply Chroma Key (Green Screen)](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/chroma-key-green-screen-1e3e99/) --- ```swift file=@cesdk_swift_examples/engine-guides-chroma-key-green-screen/ChromaKeyGreenScreen.swift reference-only import Foundation import IMGLYEngine @MainActor func chromaKeyGreenScreen(engine: Engine) async throws { // Demo scaffolding: a scene with one page, plus synthesized green-screen // footage — an astronaut sticker flattened onto a uniform green backdrop, // exported into an engine buffer — so the example has a keyable frame to work with. 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 let backdrop = try engine.block.create(.graphic) try engine.block.setShape(backdrop, shape: engine.block.createShape(.rect)) let backdropFill = try engine.block.createFill(.color) try engine.block.setColor(backdropFill, property: "fill/color/value", color: .rgba(r: 0, g: 0.8, b: 0.25, a: 1)) try engine.block.setFill(backdrop, fill: backdropFill) try engine.block.setWidth(backdrop, value: 800) try engine.block.setHeight(backdrop, value: 600) try engine.block.setPositionX(backdrop, value: 0) try engine.block.setPositionY(backdrop, value: 0) try engine.block.appendChild(to: page, child: backdrop) let subject = try engine.block.create(.graphic) try engine.block.setShape(subject, shape: engine.block.createShape(.rect)) let subjectFill = try engine.block.createFill(.image) try engine.block.setURL( subjectFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.sticker/images/3Dstickers/3d_stickers_astronaut.png"), ) try engine.block.setFill(subject, fill: subjectFill) try engine.block.setWidth(subject, value: 360) try engine.block.setHeight(subject, value: 400) try engine.block.setPositionX(subject, value: 220) try engine.block.setPositionY(subject, value: 130) try engine.block.appendChild(to: page, child: subject) let frameData = try await engine.block.export(page, mimeType: .png) // Keep the buffer alive while the image fill references it. // Destroy it when the fill is no longer needed. let frameURL = engine.editor.createBuffer() try engine.editor.setBufferData(url: frameURL, offset: 0, data: frameData) try engine.block.destroy(backdrop) try engine.block.destroy(subject) 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: frameURL) try engine.block.setFill(imageBlock, fill: imageFill) try engine.block.setWidth(imageBlock, value: 600) try engine.block.setHeight(imageBlock, value: 450) try engine.block.setPositionX(imageBlock, value: 100) try engine.block.setPositionY(imageBlock, value: 75) try engine.block.appendChild(to: page, child: imageBlock) try await engine.captureGuide(page, label: "before-key") let greenScreenEffect = try engine.block.createEffect(.greenScreen) try engine.block.appendEffect(imageBlock, effectID: greenScreenEffect) try engine.block.setColor( greenScreenEffect, property: "effect/green_screen/fromColor", color: .rgba(r: 0, g: 0.8, b: 0.25, a: 1), ) try await engine.captureGuide(page, label: "after-color") try engine.block.setFloat(greenScreenEffect, property: "effect/green_screen/colorMatch", value: 0.26) try engine.block.setFloat(greenScreenEffect, property: "effect/green_screen/smoothness", value: 0.15) try engine.block.setFloat(greenScreenEffect, property: "effect/green_screen/spill", value: 0.4) try await engine.captureGuide(page, label: "after-tuning") 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: 1), ) try engine.block.setFill(backgroundBlock, fill: backgroundFill) try engine.block.setWidth(backgroundBlock, value: 800) try engine.block.setHeight(backgroundBlock, value: 600) try engine.block.setPositionX(backgroundBlock, value: 0) try engine.block.setPositionY(backgroundBlock, value: 0) try engine.block.appendChild(to: page, child: backgroundBlock) try engine.block.sendToBack(backgroundBlock) try engine.block.bringToFront(imageBlock) try await engine.captureGuide(page, label: "hero") let isEnabled = try engine.block.isEffectEnabled(effectID: greenScreenEffect) print("Green screen effect enabled: \(isEnabled)") try engine.block.setEffectEnabled(effectID: greenScreenEffect, enabled: !isEnabled) let blockSupportsEffects = try engine.block.supportsEffects(imageBlock) print("Block supports effects: \(blockSupportsEffects)") let effects = try engine.block.getEffects(imageBlock) print("Number of effects: \(effects.count)") if let effectIndex = effects.firstIndex(of: greenScreenEffect) { try engine.block.removeEffect(imageBlock, index: effectIndex) } try engine.block.destroy(greenScreenEffect) } ``` Replace specific colors with transparency using CE.SDK's green screen effect for video compositing and virtual background applications. ![An astronaut subject composited over a solid blue background after the green backdrop of the source frame was keyed out with the green screen effect](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-chroma-key-green-screen) The green screen effect (chroma key) replaces a specified color with transparency, enabling compositing workflows where foreground subjects appear over different backgrounds. While green is the most common key color due to its contrast with skin tones, the effect works with any solid color—blue screens, white backgrounds, or custom colors. CE.SDK processes chroma keying in real-time using GPU-accelerated shaders. This guide covers how to apply the green screen effect programmatically, configure color selection and keying parameters, composite with background layers, and manage effects on blocks. The example applies the effect to a graphic block whose image fill holds a frame of green-screen footage — a subject in front of a uniform green backdrop — and keys out the backdrop. ## Apply the Green Screen Effect Create a green screen effect instance with `createEffect(_:)` and attach it to a block with `appendEffect(_:effectID:)`, which adds the effect to the block's effect list. The effect immediately processes the target color, making matching pixels transparent. ```swift highlight-chromaKey-createEffect let greenScreenEffect = try engine.block.createEffect(.greenScreen) try engine.block.appendEffect(imageBlock, effectID: greenScreenEffect) ``` `imageBlock` is the example's graphic block with an image fill; the same calls work on the block types that support effects — graphic blocks and pages. Video content also lives on a graphic block, with a video fill instead of an image fill, so the same workflow applies. ## Configure Color Selection The green screen effect targets a specific color to key out. Set this color using `setColor(_:property:color:)` with the `effect/green_screen/fromColor` property. The effect defaults to pure green, and the color's alpha channel is ignored. ```swift highlight-chromaKey-configureColor try engine.block.setColor( greenScreenEffect, property: "effect/green_screen/fromColor", color: .rgba(r: 0, g: 0.8, b: 0.25, a: 1), ) ``` The example sets the key color to the exact green of its footage's backdrop. For blue screen footage, set the color to blue instead — any solid color works. Match the exact color you want to remove for best results. ## Adjust Color Matching Tolerance The `colorMatch` parameter controls how closely pixels must match the target color to be keyed out. Adjust it with `setFloat(_:property:value:)`. ```swift highlight-chromaKey-colorMatch try engine.block.setFloat(greenScreenEffect, property: "effect/green_screen/colorMatch", value: 0.26) ``` Higher values (closer to `1.0`) key out a wider range of similar colors, which is useful for footage with uneven lighting or color variations in the background. Lower values create more precise keying for well-lit footage with uniform backgrounds. The parameter ranges from `0.0` to `1.0` and defaults to `0.4`. ## Control Edge Smoothness The `smoothness` parameter controls the transition between opaque and transparent areas. This affects how sharp or soft the edges appear around keyed subjects. ```swift highlight-chromaKey-smoothness try engine.block.setFloat(greenScreenEffect, property: "effect/green_screen/smoothness", value: 0.15) ``` Higher smoothness values create softer edges that blend naturally with new backgrounds, reducing harsh outlines. Lower values produce sharper edges, which may be preferable for high-contrast composites or when preserving fine detail. ## Remove Color Spill Color spill occurs when the key color reflects onto the foreground subject, creating a green or blue tint on edges. The `spill` parameter desaturates the remaining traces of the key color. ```swift highlight-chromaKey-spill try engine.block.setFloat(greenScreenEffect, property: "effect/green_screen/spill", value: 0.4) ``` Increase the spill value when you notice the key color appearing on subject edges or reflective surfaces. This is common with shiny hair, glasses, or metallic objects near the screen. Spill removal is off by default (`0.0`). ## Composite with Background Layers After keying, layer the transparent content over backgrounds using block ordering. Create a background block and use `sendToBack(_:)` to place it behind the keyed image. ```swift highlight-chromaKey-composite 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: 1), ) try engine.block.setFill(backgroundBlock, fill: backgroundFill) try engine.block.setWidth(backgroundBlock, value: 800) try engine.block.setHeight(backgroundBlock, value: 600) try engine.block.setPositionX(backgroundBlock, value: 0) try engine.block.setPositionY(backgroundBlock, value: 0) try engine.block.appendChild(to: page, child: backgroundBlock) try engine.block.sendToBack(backgroundBlock) try engine.block.bringToFront(imageBlock) ``` The background appears through the transparent areas where the key color was removed. You can use image or video fills instead of solid colors for more dynamic backgrounds. ## Toggle the Effect Check whether an effect is enabled using `isEffectEnabled(effectID:)`. ```swift highlight-chromaKey-checkEnabled let isEnabled = try engine.block.isEffectEnabled(effectID: greenScreenEffect) print("Green screen effect enabled: \(isEnabled)") ``` To toggle the effect on or off, use `setEffectEnabled(effectID:enabled:)`. This preserves the effect configuration while temporarily removing its visual impact — here the effect is flipped relative to the state read above. ```swift highlight-chromaKey-setEnabled try engine.block.setEffectEnabled(effectID: greenScreenEffect, enabled: !isEnabled) ``` Toggling effects is useful for before/after comparisons or conditional processing without removing and recreating the effect. ## Manage the Effect Beyond toggling, you can query, remove, and clean up effects. Use `supportsEffects(_:)` to check if a block can have effects, `getEffects(_:)` to list all applied effects, `removeEffect(_:index:)` to detach an effect from a block, and `destroy(_:)` to free the effect's resources. ```swift highlight-chromaKey-manageEffects let blockSupportsEffects = try engine.block.supportsEffects(imageBlock) print("Block supports effects: \(blockSupportsEffects)") let effects = try engine.block.getEffects(imageBlock) print("Number of effects: \(effects.count)") if let effectIndex = effects.firstIndex(of: greenScreenEffect) { try engine.block.removeEffect(imageBlock, index: effectIndex) } try engine.block.destroy(greenScreenEffect) ``` When removing an effect, find its position in the list returned by `getEffects(_:)` and pass that index to `removeEffect(_:index:)`. Removing an effect detaches it from the block but keeps the instance alive — call `destroy(_:)` on the effect to release its resources. ## Troubleshooting ### Keying Results Appear Rough or Incomplete - Increase the `colorMatch` value to capture more color variations - Ensure source footage has even lighting on the screen - Check that the target color accurately matches the screen color ### Edges Have Color Fringing - Increase the `spill` value to remove color cast - Adjust `smoothness` to soften hard edges - Increase `colorMatch` if the fringe consists of leftover key-color pixels that fall just outside the matching threshold ### Transparent Areas Appear in Wrong Places - Decrease `colorMatch` to be more selective about which colors are keyed - Verify the `fromColor` matches only the intended background color - Check that foreground subjects don't contain colors similar to the key color ## API Reference ### Methods | Method | Description | | -------------------------------------------- | ------------------------------------------------------------------ | | `engine.block.createEffect(_:)` | Create an effect instance from an `EffectType` such as `.greenScreen` | | `engine.block.appendEffect(_:effectID:)` | Add an effect to the end of a block's effect list | | `engine.block.setColor(_:property:color:)` | Set the color to key out | | `engine.block.setFloat(_:property:value:)` | Set a keying parameter such as tolerance, smoothness, or spill | | `engine.block.isEffectEnabled(effectID:)` | Check whether an effect is enabled | | `engine.block.setEffectEnabled(effectID:enabled:)` | Enable or disable an effect | | `engine.block.supportsEffects(_:)` | Check whether a block supports effects | | `engine.block.getEffects(_:)` | Get all effects applied to a block | | `engine.block.removeEffect(_:index:)` | Remove the effect at the given position from a block | | `engine.block.destroy(_:)` | Destroy an effect instance | ### Properties | Property | Type | Description | | --------------------------------- | ----- | --------------------------------------------------------------------------- | | `effect/green_screen/fromColor` | Color | The color to replace with transparency; defaults to pure green, alpha is ignored | | `effect/green_screen/colorMatch` | Float | Color matching tolerance (`0.0`–`1.0`, default `0.4`) | | `effect/green_screen/smoothness` | Float | Edge smoothness (`0.0`–`1.0`, default `0.08`) | | `effect/green_screen/spill` | Float | Spill removal intensity (`0.0`–`1.0`, default `0.0`) | ## Next Steps - [Apply a Filter or Effect](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) — Apply, configure, stack, and manage filters and effects with the Engine API. - [Blur Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/blur-71d642/) — Soften backgrounds and create depth with the blur API. - [Duotone](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/duotone-831fc5/) — Map image tones to two colors for stylized or vintage treatments. --- ## 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 Custom Filters" description: "Extend CE.SDK with custom LUT filter asset sources for brand-specific color grading and filter collections." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-filters-c796ba/" --- > 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/) > [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) > [Create Custom Filters](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-filters-c796ba/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-custom-filters/CreateCustomFilters.swift reference-only import Foundation import IMGLYEngine @MainActor func createCustomFilters(engine: Engine) async throws { // Demo scaffolding: a design scene with a page and two image blocks to apply // filters to. In your app these would be existing design elements. 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 sampleImage = baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg") let firstImage = try engine.block.create(.graphic) try engine.block.setShape(firstImage, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(firstImage, value: 50) try engine.block.setPositionY(firstImage, value: 188) try engine.block.setWidth(firstImage, value: 300) try engine.block.setHeight(firstImage, value: 225) let firstFill = try engine.block.createFill(.image) try engine.block.setURL(firstFill, property: "fill/image/imageFileURI", value: sampleImage) try engine.block.setFill(firstImage, fill: firstFill) try engine.block.appendChild(to: page, child: firstImage) let secondImage = try engine.block.create(.graphic) try engine.block.setShape(secondImage, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(secondImage, value: 450) try engine.block.setPositionY(secondImage, value: 188) try engine.block.setWidth(secondImage, value: 300) try engine.block.setHeight(secondImage, value: 225) let secondFill = try engine.block.createFill(.image) try engine.block.setURL(secondFill, property: "fill/image/imageFileURI", value: sampleImage) try engine.block.setFill(secondImage, fill: secondFill) try engine.block.appendChild(to: page, child: secondImage) let warmLUT = baseURL.appendingPathComponent("ly.img.filter.lut/LUTs/imgly_lut_sepia_5_5_128.png") let monochromeLUT = baseURL.appendingPathComponent("ly.img.filter.lut/LUTs/imgly_lut_bw_5_5_128.png") // Register a local asset source to hold the brand's custom filters. try engine.asset.addLocalSource(sourceID: "my-custom-filters") // Define each filter with its LUT metadata and add it to the source. let vintageWarm = AssetDefinition( id: "vintage-warm", groups: ["Warm Tones"], meta: [ "uri": warmLUT.absoluteString, "thumbUri": warmLUT.absoluteString, "horizontalTileCount": "5", "verticalTileCount": "5", "blockType": EffectType.lutFilter.rawValue, ], label: ["en": "Vintage Warm"], tags: ["en": ["vintage", "warm", "retro"]], ) try engine.asset.addAsset(to: "my-custom-filters", asset: vintageWarm) let monochromeClassic = AssetDefinition( id: "monochrome-classic", groups: ["Monochrome"], meta: [ "uri": monochromeLUT.absoluteString, "thumbUri": monochromeLUT.absoluteString, "horizontalTileCount": "5", "verticalTileCount": "5", "blockType": EffectType.lutFilter.rawValue, ], label: ["en": "Monochrome Classic"], tags: ["en": ["monochrome", "classic", "black and white"]], ) try engine.asset.addAsset(to: "my-custom-filters", asset: monochromeClassic) let filterConfigJSON = """ { "version": "2.0.0", "id": "my-json-filters", "assets": [ { "id": "noir-classic", "label": { "en": "Noir Classic" }, "tags": { "en": ["monochrome", "noir", "grayscale"] }, "groups": ["Monochrome"], "meta": { "uri": "\(monochromeLUT.absoluteString)", "thumbUri": "\(monochromeLUT.absoluteString)", "horizontalTileCount": "5", "verticalTileCount": "5", "blockType": "\(EffectType.lutFilter.rawValue)" } }, { "id": "sunset-glow", "label": { "en": "Sunset Glow" }, "tags": { "en": ["warm", "sunset", "golden"] }, "groups": ["Warm Tones"], "meta": { "uri": "\(warmLUT.absoluteString)", "thumbUri": "\(warmLUT.absoluteString)", "horizontalTileCount": "5", "verticalTileCount": "5", "blockType": "\(EffectType.lutFilter.rawValue)" } } ] } """ let jsonSourceID = try engine.asset.addLocalAssetSourceFromJSON(filterConfigJSON) print("Created JSON-based filter source: \(jsonSourceID)") let customResults = try await engine.asset.findAssets( sourceID: "my-custom-filters", query: .init(query: nil, page: 0, perPage: 10), ) print("Found \(customResults.total) filters in the custom source") // Narrow the results to a single category with the groups parameter. let warmResults = try await engine.asset.findAssets( sourceID: "my-custom-filters", query: .init(query: nil, page: 0, groups: ["Warm Tones"], perPage: 10), ) print("Found \(warmResults.total) warm-tone filters") let jsonResults = try await engine.asset.findAssets( sourceID: jsonSourceID, query: .init(query: nil, page: 0, perPage: 10), ) print("Found \(jsonResults.total) filters in the JSON source") let monochromeResults = try await engine.asset.findAssets( sourceID: jsonSourceID, query: .init(query: nil, page: 0, groups: ["Monochrome"], perPage: 10), ) print("Found \(monochromeResults.total) monochrome filters") let allSources = engine.asset.findAllSources() print("Registered sources: \(allSources)") if let filter = warmResults.assets.first, let meta = filter.meta, let lutURL = meta["uri"].flatMap(URL.init(string:)) { let lutEffect = try engine.block.createEffect(.lutFilter) try engine.block.setURL(lutEffect, property: "effect/lut_filter/lutFileURI", value: lutURL) try engine.block.setInt( lutEffect, property: "effect/lut_filter/horizontalTileCount", value: Int(meta["horizontalTileCount"] ?? "") ?? 5, ) try engine.block.setInt( lutEffect, property: "effect/lut_filter/verticalTileCount", value: Int(meta["verticalTileCount"] ?? "") ?? 5, ) try engine.block.setFloat(lutEffect, property: "effect/lut_filter/intensity", value: 0.85) try engine.block.appendEffect(firstImage, effectID: lutEffect) } try await engine.captureGuide(page, label: "after-apply") if let filter = monochromeResults.assets.first, let meta = filter.meta, let lutURL = meta["uri"].flatMap(URL.init(string:)) { let lutEffect = try engine.block.createEffect(.lutFilter) try engine.block.setURL(lutEffect, property: "effect/lut_filter/lutFileURI", value: lutURL) try engine.block.setInt( lutEffect, property: "effect/lut_filter/horizontalTileCount", value: Int(meta["horizontalTileCount"] ?? "") ?? 5, ) try engine.block.setInt( lutEffect, property: "effect/lut_filter/verticalTileCount", value: Int(meta["verticalTileCount"] ?? "") ?? 5, ) try engine.block.setFloat(lutEffect, property: "effect/lut_filter/intensity", value: 0.85) try engine.block.appendEffect(secondImage, effectID: lutEffect) } // Most-evolved scene — promoted to the guide's hero image. try await engine.captureGuide(page, label: "hero") let blob = try await engine.block.export(page, mimeType: .png) let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("custom-filters.png") try blob.write(to: outputURL) } ``` Extend CE.SDK with your own LUT filters by creating and registering custom filter asset sources for brand-specific color grading. ![The same photo shown twice: on the left a custom warm sepia LUT filter, on the right a custom black-and-white monochrome LUT filter.](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-create-custom-filters) CE.SDK provides built-in LUT filters, but many applications need brand-specific color grading or custom filter collections. Custom filter asset sources let you register your own LUT filters that can be queried and applied programmatically, alongside the built-in defaults. This guide covers how to define filter metadata, register a filter asset source, load filters from JSON configuration, query the source, and apply filters to design elements. ## Filter Asset Metadata LUT filters need these properties in the `meta` dictionary of each asset: - **`uri`** - URL to the LUT image file (PNG format) - **`thumbUri`** - URL to the preview thumbnail shown in a filter picker. In production this should point at a rendered preview (a sample image with the filter applied); the examples here reuse the LUT image for brevity, which would otherwise show the raw LUT grid in a picker - **`horizontalTileCount`** - Number of horizontal tiles in the LUT grid (typically 5 or 8) - **`verticalTileCount`** - Number of vertical tiles in the LUT grid (typically 5 or 8) - **`blockType`** - Must be `//ly.img.ubq/effect/lut_filter` for LUT filters, the value of `EffectType.lutFilter.rawValue` `meta` is a `[String: String]` dictionary, so numeric values like the tile counts are stored as strings. ## Adding a Custom Filter Register a local asset source with `engine.asset.addLocalSource(sourceID:)`, then add each filter to it with `engine.asset.addAsset(to:asset:)`. Each filter is an `AssetDefinition` carrying its display label, search tags, category groups, and the LUT configuration in `meta`. ```swift highlight-createCustomFilters-createSource let warmLUT = baseURL.appendingPathComponent("ly.img.filter.lut/LUTs/imgly_lut_sepia_5_5_128.png") let monochromeLUT = baseURL.appendingPathComponent("ly.img.filter.lut/LUTs/imgly_lut_bw_5_5_128.png") // Register a local asset source to hold the brand's custom filters. try engine.asset.addLocalSource(sourceID: "my-custom-filters") // Define each filter with its LUT metadata and add it to the source. let vintageWarm = AssetDefinition( id: "vintage-warm", groups: ["Warm Tones"], meta: [ "uri": warmLUT.absoluteString, "thumbUri": warmLUT.absoluteString, "horizontalTileCount": "5", "verticalTileCount": "5", "blockType": EffectType.lutFilter.rawValue, ], label: ["en": "Vintage Warm"], tags: ["en": ["vintage", "warm", "retro"]], ) try engine.asset.addAsset(to: "my-custom-filters", asset: vintageWarm) let monochromeClassic = AssetDefinition( id: "monochrome-classic", groups: ["Monochrome"], meta: [ "uri": monochromeLUT.absoluteString, "thumbUri": monochromeLUT.absoluteString, "horizontalTileCount": "5", "verticalTileCount": "5", "blockType": EffectType.lutFilter.rawValue, ], label: ["en": "Monochrome Classic"], tags: ["en": ["monochrome", "classic", "black and white"]], ) try engine.asset.addAsset(to: "my-custom-filters", asset: monochromeClassic) ``` ### Filter Asset Structure Each `AssetDefinition` needs: - **`id`** - Unique identifier for the filter - **`label`** - Localized display name. `IMGLYEngine.Locale` is a `String` typealias, so use plain language keys such as `["en": "Vintage Warm"]` - **`tags`** - Localized keywords for free-text search - **`groups`** - Category assignments for filtering and organization - **`meta`** - The LUT configuration (uri, thumbUri, tile counts, blockType) ## Loading Filters from JSON Configuration For larger filter collections, load definitions from a JSON string with `engine.asset.addLocalAssetSourceFromJSON(_:)`. It returns the source ID declared in the JSON, keeping filter libraries in configuration files instead of code. ```swift highlight-createCustomFilters-loadJSON let filterConfigJSON = """ { "version": "2.0.0", "id": "my-json-filters", "assets": [ { "id": "noir-classic", "label": { "en": "Noir Classic" }, "tags": { "en": ["monochrome", "noir", "grayscale"] }, "groups": ["Monochrome"], "meta": { "uri": "\(monochromeLUT.absoluteString)", "thumbUri": "\(monochromeLUT.absoluteString)", "horizontalTileCount": "5", "verticalTileCount": "5", "blockType": "\(EffectType.lutFilter.rawValue)" } }, { "id": "sunset-glow", "label": { "en": "Sunset Glow" }, "tags": { "en": ["warm", "sunset", "golden"] }, "groups": ["Warm Tones"], "meta": { "uri": "\(warmLUT.absoluteString)", "thumbUri": "\(warmLUT.absoluteString)", "horizontalTileCount": "5", "verticalTileCount": "5", "blockType": "\(EffectType.lutFilter.rawValue)" } } ] } """ let jsonSourceID = try engine.asset.addLocalAssetSourceFromJSON(filterConfigJSON) print("Created JSON-based filter source: \(jsonSourceID)") ``` ### JSON Structure for Filter Assets The JSON includes: - **`version`** - Schema version (use `"2.0.0"`) - **`id`** - Unique source identifier - **`assets`** - Array of filter definitions Each asset contains an `id`, a localized `label` object (e.g. `{ "en": "Sunset Glow" }`), localized `tags`, `groups`, and a `meta` object with the LUT configuration. For filters hosted on a CDN, pass a `URL` to `engine.asset.addLocalAssetSourceFromJSON(_:)` instead, which loads the JSON from a URL and resolves relative URLs against the JSON file's parent directory. ## Querying and Applying Filters Query a source's filters with `engine.asset.findAssets(sourceID:query:)`. The `AssetQueryData` controls pagination (`page`, `perPage`), free-text search (`query`), and category filters (`groups`). `findAllSources()` lists every registered source ID. ```swift highlight-createCustomFilters-query let customResults = try await engine.asset.findAssets( sourceID: "my-custom-filters", query: .init(query: nil, page: 0, perPage: 10), ) print("Found \(customResults.total) filters in the custom source") // Narrow the results to a single category with the groups parameter. let warmResults = try await engine.asset.findAssets( sourceID: "my-custom-filters", query: .init(query: nil, page: 0, groups: ["Warm Tones"], perPage: 10), ) print("Found \(warmResults.total) warm-tone filters") let jsonResults = try await engine.asset.findAssets( sourceID: jsonSourceID, query: .init(query: nil, page: 0, perPage: 10), ) print("Found \(jsonResults.total) filters in the JSON source") let monochromeResults = try await engine.asset.findAssets( sourceID: jsonSourceID, query: .init(query: nil, page: 0, groups: ["Monochrome"], perPage: 10), ) print("Found \(monochromeResults.total) monochrome filters") let allSources = engine.asset.findAllSources() print("Registered sources: \(allSources)") ``` Build a LUT filter effect from a queried filter's `meta`: create a `lut_filter` effect, set its LUT file URL and tile counts from the metadata, set the intensity, then attach it to a graphic block with `appendEffect(_:effectID:)`. `firstImage` here is a graphic block with an image fill. ```swift highlight-createCustomFilters-apply if let filter = warmResults.assets.first, let meta = filter.meta, let lutURL = meta["uri"].flatMap(URL.init(string:)) { let lutEffect = try engine.block.createEffect(.lutFilter) try engine.block.setURL(lutEffect, property: "effect/lut_filter/lutFileURI", value: lutURL) try engine.block.setInt( lutEffect, property: "effect/lut_filter/horizontalTileCount", value: Int(meta["horizontalTileCount"] ?? "") ?? 5, ) try engine.block.setInt( lutEffect, property: "effect/lut_filter/verticalTileCount", value: Int(meta["verticalTileCount"] ?? "") ?? 5, ) try engine.block.setFloat(lutEffect, property: "effect/lut_filter/intensity", value: 0.85) try engine.block.appendEffect(firstImage, effectID: lutEffect) } ``` The same recipe applies to filters from any registered source, including the JSON-based source. ```swift highlight-createCustomFilters-applyJSON if let filter = monochromeResults.assets.first, let meta = filter.meta, let lutURL = meta["uri"].flatMap(URL.init(string:)) { let lutEffect = try engine.block.createEffect(.lutFilter) try engine.block.setURL(lutEffect, property: "effect/lut_filter/lutFileURI", value: lutURL) try engine.block.setInt( lutEffect, property: "effect/lut_filter/horizontalTileCount", value: Int(meta["horizontalTileCount"] ?? "") ?? 5, ) try engine.block.setInt( lutEffect, property: "effect/lut_filter/verticalTileCount", value: Int(meta["verticalTileCount"] ?? "") ?? 5, ) try engine.block.setFloat(lutEffect, property: "effect/lut_filter/intensity", value: 0.85) try engine.block.appendEffect(secondImage, effectID: lutEffect) } ``` After applying the filters, export the page to a PNG. ```swift highlight-createCustomFilters-export let blob = try await engine.block.export(page, mimeType: .png) let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("custom-filters.png") try blob.write(to: outputURL) ``` ## Troubleshooting ### Filters Not Found in Query - Add filters with `addAsset(to:asset:)` (or load them with `addLocalAssetSourceFromJSON(_:)`) before calling `findAssets(sourceID:query:)` - Check that the source ID matches the one you registered - Confirm each filter's `meta` includes all required fields ### LUT Not Rendering Correctly - Verify the tile count values match the actual LUT image grid dimensions - Check that the LUT image URL is reachable - Confirm the LUT image is a PNG ### JSON Source Not Loading - Verify the JSON has `version`, `id`, and `assets` - Ensure each asset's `meta` includes the required fields - Check for JSON syntax errors ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addLocalSource(sourceID:)` | Register a local asset source | | `engine.asset.addAsset(to:asset:)` | Add an `AssetDefinition` to a registered source | | `engine.asset.addLocalAssetSourceFromJSON(_:basePath:matcher:)` | Create an asset source from an inline JSON string | | `engine.asset.addLocalAssetSourceFromJSON(_:matcher:)` | Load an asset source from a JSON file URL | | `engine.asset.findAssets(sourceID:query:)` | Query a source's assets with paging, search, and groups | | `engine.asset.findAllSources()` | List registered asset source IDs | | `engine.block.createEffect(_:)` | Create an effect (use `.lutFilter` for LUT filters) | | `engine.block.setURL(_:property:value:)` | Set the LUT file URL | | `engine.block.setInt(_:property:value:)` | Set the tile counts | | `engine.block.setFloat(_:property:value:)` | Set the filter intensity | | `engine.block.appendEffect(_:effectID:)` | Add the effect to a block's effect stack | ### Properties | Property | Type | Description | | --- | --- | --- | | `effect/lut_filter/lutFileURI` | URL | LUT image file URL | | `effect/lut_filter/horizontalTileCount` | Int | Horizontal tiles in the LUT grid | | `effect/lut_filter/verticalTileCount` | Int | Vertical tiles in the LUT grid | | `effect/lut_filter/intensity` | Float | Blend strength in the range `0.0`–`1.0`; `0.0` applies no filtering and `1.0` applies the LUT fully | ## Next Steps - [Apply Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) - Learn to apply filters to design elements and manage effect stacks - [Create a Custom LUT Filter](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-lut-filter-6e3f49/) - Understand LUT image format and create your own color grading filters - [Blur Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/blur-71d642/) - Add blur effects to images and videos --- ## 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 Custom LUT Filter" description: "Create and apply custom LUT filters to achieve consistent, brand-aligned visual styles." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-lut-filter-6e3f49/" --- > 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/) > [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) > [Apply Custom LUT Filter](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-lut-filter-6e3f49/) --- ```swift file=@cesdk_swift_examples/engine-guides-custom-lut-filter/CustomLUTFilter.swift reference-only import Foundation import IMGLYEngine @MainActor func customLutFilter(engine: Engine) async throws { // Demo scaffolding: a scene, a page, and an image block to grade. In your // app this is whatever image block the user is editing. 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 engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(imageBlock, value: 800) try engine.block.setHeight(imageBlock, value: 600) 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: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) // The URL of your hosted (or app-bundled) tiled-PNG LUT image. let lutURL = baseURL.appendingPathComponent("ly.img.filter.lut/LUTs/imgly_lut_ad1920_5_5_128.png") try await engine.captureGuide(page, label: "before-lut") let lutEffect = try engine.block.createEffect(.lutFilter) try engine.block.setURL(lutEffect, property: "effect/lut_filter/lutFileURI", value: lutURL) try engine.block.setInt(lutEffect, property: "effect/lut_filter/horizontalTileCount", value: 5) try engine.block.setInt(lutEffect, property: "effect/lut_filter/verticalTileCount", value: 5) try engine.block.setFloat(lutEffect, property: "effect/lut_filter/intensity", value: 0.9) try engine.block.appendEffect(imageBlock, effectID: lutEffect) try await engine.captureGuide(page, label: "hero") try engine.block.setEffectEnabled(effectID: lutEffect, enabled: false) let isEnabled = try engine.block.isEffectEnabled(effectID: lutEffect) print("LUT filter enabled: \(isEnabled)") try engine.block.setEffectEnabled(effectID: lutEffect, enabled: true) let supportsEffects = try engine.block.supportsEffects(imageBlock) let effects = try engine.block.getEffects(imageBlock) print("Supports effects: \(supportsEffects), count: \(effects.count)") } ``` Apply custom LUT (Look-Up Table) filters to achieve brand-consistent color grading directly through CE.SDK's effect API. ![An image block with a custom LUT color grade applied through the effect API](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-custom-lut-filter) LUT filters remap colors through a predefined transformation table, enabling cinematic color grading and consistent brand aesthetics. This guide shows how to apply your own LUT files directly to design elements using the effect API. To organize collections of filters as reusable assets, see [Create Custom Filters](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-filters-c796ba/) for registering them as custom asset sources. This guide covers CE.SDK's tiled-PNG LUT format, hosting your LUT image, creating and configuring a `lut_filter` effect, applying it to an image block, and toggling and inspecting effects after they're applied. ## Understanding LUT Image Format CE.SDK uses a tiled PNG format where a 3D color cube is laid out as a 2D grid. Each tile represents a slice of the color cube along the blue axis. The LUT image requires two configuration values: - **`horizontalTileCount`** - Number of tiles across the image width - **`verticalTileCount`** - Number of tiles down the image height CE.SDK supports these tile configurations: - 5×5 tiles with 128px cube size - 8×8 tiles with 512px cube size Standard `.cube` files must be converted to this tiled PNG format using image processing tools. ## Creating LUT PNG Images ### Starting from the Identity LUT The fastest way to author a custom LUT filter is to edit the **identity LUT**: a neutral 8×8 tiled PNG (512px cube size) that produces no color change when applied. Any color adjustments you make to this image are recorded as the filter's transformation, and the resulting PNG can be used directly with the `lut_filter` effect. Identity LUT To author a new filter from this identity LUT: 1. [Download the identity LUT](content-assets/6e3f49/identity.png) 2. Open it in an image editor that operates on the whole image (Adobe Photoshop, Affinity Photo, GIMP, Pixelmator Pro) 3. Apply color adjustments — curves, levels, hue/saturation, color balance, channel mixer — to the entire image 4. Export the result as PNG; the exported file is your custom LUT Do not crop, rotate, resize, or otherwise change the geometry of the image. Each pixel in the identity LUT is a specific color sample; reorganizing pixels breaks the color mapping. > **WARNING:** Save the edited LUT as PNG. JPEG and other lossy formats introduce compression artifacts that produce visible color banding when the filter is applied. ### Obtaining LUT Files LUT files are also available from multiple sources: - **Color grading software** - Adobe Photoshop, DaVinci Resolve, and Affinity Photo can export 3D LUT files in `.cube` format - **Online LUT libraries** - Many free and commercial LUT packs are available for download - **LUT generators** - Tools that create custom color transformations from reference images ### Converting .cube to Tiled PNG CE.SDK requires LUTs in a specific tiled PNG format where each tile represents a slice of the 3D color cube along the blue axis. To convert a standard `.cube` file: 1. **Parse the .cube file** - Read the 3D color lookup table data 2. **Arrange slices as tiles** - Each blue channel value becomes a separate tile containing the red-green color plane 3. **Export as PNG** - Save the grid as a PNG image CE.SDK's built-in LUTs follow a naming convention: `imgly_lut_{name}_{h}_{v}_{cubeSize}.png` where `h` and `v` are tile counts and `cubeSize` indicates the LUT precision. ### Using a Script for Conversion You can write a script using image-processing libraries (for example Python with Pillow and NumPy) to convert `.cube` files: ```python # Pseudocode for .cube to tiled PNG conversion # 1. Parse the .cube file to extract the 3D LUT data # 2. Reshape data into (blue_slices, height, width, 3) array # 3. Arrange slices in a grid matching tile configuration # 4. Save as PNG with Image.fromarray() ``` ### Using CE.SDK's Built-in LUTs You can also reference CE.SDK's built-in LUT assets as format-verified examples. The filter extension at `ly.img.filter.lut/LUTs` contains pre-generated tiled PNGs you can inspect to confirm tile counts, cube size, and overall layout when authoring or converting your own LUTs. ## Hosting LUT Files The LUT image must be reachable from the device at the URL you pass to the effect. Bundle it with your app for offline access, or serve it from your own host over HTTPS for production deployments. ## Creating the LUT Effect Create a `lut_filter` effect instance with `createEffect(_:)`. ```swift highlight-customLutFilter-createEffect let lutEffect = try engine.block.createEffect(.lutFilter) ``` This creates an effect that can be configured and applied to image blocks. ## Configuring LUT Properties Point the effect at your LUT image with `setURL(_:property:value:)` on the `effect/lut_filter/lutFileURI` property, and set the tile dimensions with `setInt(_:property:value:)`. Here `lutURL` is the URL of your tiled-PNG LUT image. ```swift highlight-customLutFilter-configure try engine.block.setURL(lutEffect, property: "effect/lut_filter/lutFileURI", value: lutURL) try engine.block.setInt(lutEffect, property: "effect/lut_filter/horizontalTileCount", value: 5) try engine.block.setInt(lutEffect, property: "effect/lut_filter/verticalTileCount", value: 5) ``` The tile counts must match the actual LUT image grid structure. Using incorrect values produces distorted colors. ## Setting Filter Intensity Control the strength of the color transformation with `setFloat(_:property:value:)` on the `effect/lut_filter/intensity` property. ```swift highlight-customLutFilter-intensity try engine.block.setFloat(lutEffect, property: "effect/lut_filter/intensity", value: 0.9) ``` Values range from `0.0` (no effect) to `1.0` (full effect). Use intermediate values for subtle color grading. ## Applying the Effect Attach the configured effect to an image block with `appendEffect(_:effectID:)`. ```swift highlight-customLutFilter-apply try engine.block.appendEffect(imageBlock, effectID: lutEffect) ``` The effect renders as soon as it is applied. ## Toggling the Effect Enable or disable the effect without removing it using `setEffectEnabled(effectID:enabled:)`, and read the current state with `isEffectEnabled(effectID:)`. ```swift highlight-customLutFilter-toggle try engine.block.setEffectEnabled(effectID: lutEffect, enabled: false) let isEnabled = try engine.block.isEffectEnabled(effectID: lutEffect) print("LUT filter enabled: \(isEnabled)") try engine.block.setEffectEnabled(effectID: lutEffect, enabled: true) ``` Disabling preserves all effect settings while temporarily removing the visual transformation. ## Managing Effects Retrieve and inspect the effects applied to a block. Use `getEffects(_:)` to access all effects on a block and `supportsEffects(_:)` to verify compatibility before applying. ```swift highlight-customLutFilter-manage let supportsEffects = try engine.block.supportsEffects(imageBlock) let effects = try engine.block.getEffects(imageBlock) print("Supports effects: \(supportsEffects), count: \(effects.count)") ``` To remove an effect, call `removeEffect(_:index:)` with its index in the list, then `destroy(_:)` to free the effect instance. ## Troubleshooting ### LUT Not Rendering - Verify the LUT image URL is reachable from the device - Confirm the image uses PNG format - Check that tile count values match the actual image grid ### Colors Look Wrong - Verify tile counts match the LUT image structure - Ensure the LUT was generated with sRGB color space ### Effect Not Visible - Verify the effect is enabled with `isEffectEnabled(effectID:)` - Ensure the effect was appended to the block, not just created - Check the block supports effects with `supportsEffects(_:)` ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.createEffect(_:)` | Create an effect instance for an `EffectType` such as `.lutFilter` | | `engine.block.appendEffect(_:effectID:)` | Apply an effect to a block | | `engine.block.getEffects(_:)` | Get all effects on a block | | `engine.block.setEffectEnabled(effectID:enabled:)` | Enable or disable an effect | | `engine.block.isEffectEnabled(effectID:)` | Check whether an effect is enabled | | `engine.block.removeEffect(_:index:)` | Remove the effect at an index | | `engine.block.destroy(_:)` | Destroy an effect instance | | `engine.block.supportsEffects(_:)` | Check whether a block supports effects | ### Properties | Property | Type | Description | | --- | --- | --- | | `effect/lut_filter/lutFileURI` | URL | URL of the tiled-PNG LUT image | | `effect/lut_filter/horizontalTileCount` | Int | Number of tiles across the image width | | `effect/lut_filter/verticalTileCount` | Int | Number of tiles down the image height | | `effect/lut_filter/intensity` | Float | Filter intensity, from `0.0` to `1.0` | ## Next Steps - [Apply Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) - Learn more about the effects system - [Create Custom Filters](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-filters-c796ba/) - Register custom LUT filters as asset sources for brand-specific color grading and filter collections. - [Duotone](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/duotone-831fc5/) - Apply duotone effects to images, mapping tones to two colors for stylized visuals, vintage aesthetics, or brand-specific treatments. --- ## 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: "Distortion Effects" description: "Apply distortion effects to warp, shift, and transform design elements for dynamic artistic visuals in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/distortion-5b5a66/" --- > 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/) > [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) > [Distortion](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/distortion-5b5a66/) --- ```swift file=@cesdk_swift_examples/engine-guides-distortion/Distortion.swift reference-only import Foundation import IMGLYEngine @MainActor func distortion(engine: Engine) async throws { // Demo scaffolding: a comparison grid of six copies of the same image. The // teaching sections below apply one distortion effect to each cell so the // effects can be seen side by side; the top-left cell is left unaltered as a // reference. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1180) try engine.block.setHeight(page, value: 620) try engine.block.appendChild(to: scene, child: page) let baseURL = try engine.guidesBaseURL let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") func makeImageCell(x: Float, y: Float) throws -> DesignBlockID { let cell = try engine.block.create(.graphic) try engine.block.setShape(cell, shape: engine.block.createShape(.rect)) try engine.block.setWidth(cell, value: 360) try engine.block.setHeight(cell, value: 270) try engine.block.setPositionX(cell, value: x) try engine.block.setPositionY(cell, value: y) let fill = try engine.block.createFill(.image) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: imageURL) try engine.block.setFill(cell, fill: fill) try engine.block.setEnum(cell, property: "contentFill/mode", value: "Cover") try engine.block.appendChild(to: page, child: cell) return cell } _ = try makeImageCell(x: 30, y: 30) // original, no effect let liquidBlock = try makeImageCell(x: 410, y: 30) let mirrorBlock = try makeImageCell(x: 790, y: 30) let shifterBlock = try makeImageCell(x: 30, y: 320) let radialPixelBlock = try makeImageCell(x: 410, y: 320) let tvGlitchBlock = try makeImageCell(x: 790, y: 320) let canHaveEffects = try engine.block.supportsEffects(liquidBlock) print("Block supports effects: \(canHaveEffects)") let liquid = try engine.block.createEffect(.liquid) try engine.block.setFloat(liquid, property: "effect/liquid/amount", value: 0.5) try engine.block.setFloat(liquid, property: "effect/liquid/scale", value: 1.0) try engine.block.appendEffect(liquidBlock, effectID: liquid) let mirror = try engine.block.createEffect(.mirror) try engine.block.setInt(mirror, property: "effect/mirror/side", value: 0) try engine.block.appendEffect(mirrorBlock, effectID: mirror) let shifter = try engine.block.createEffect(.shifter) try engine.block.setFloat(shifter, property: "effect/shifter/amount", value: 0.3) try engine.block.setFloat(shifter, property: "effect/shifter/angle", value: 0.785) try engine.block.appendEffect(shifterBlock, effectID: shifter) let radialPixel = try engine.block.createEffect(.radialPixel) try engine.block.setFloat(radialPixel, property: "effect/radial_pixel/radius", value: 0.5) try engine.block.setFloat(radialPixel, property: "effect/radial_pixel/segments", value: 0.5) try engine.block.appendEffect(radialPixelBlock, effectID: radialPixel) let tvGlitch = try engine.block.createEffect(.tvGlitch) try engine.block.setFloat(tvGlitch, property: "effect/tv_glitch/distortion", value: 0.4) try engine.block.setFloat(tvGlitch, property: "effect/tv_glitch/distortion2", value: 0.2) try engine.block.setFloat(tvGlitch, property: "effect/tv_glitch/speed", value: 0.5) try engine.block.setFloat(tvGlitch, property: "effect/tv_glitch/rollSpeed", value: 0.5) try engine.block.appendEffect(tvGlitchBlock, effectID: tvGlitch) try await engine.captureGuide(page, label: "hero") let extraShifter = try engine.block.createEffect(.shifter) try engine.block.setFloat(extraShifter, property: "effect/shifter/amount", value: 0.2) try engine.block.appendEffect(liquidBlock, effectID: extraShifter) try await engine.captureGuide(liquidBlock, label: "after-combine") let effects = try engine.block.getEffects(liquidBlock) print("Applied effects: \(effects.count)") try engine.block.setEffectEnabled(effectID: extraShifter, enabled: false) let shifterEnabled = try engine.block.isEffectEnabled(effectID: extraShifter) print("Shifter enabled: \(shifterEnabled)") try engine.block.removeEffect(liquidBlock, index: 1) try engine.block.destroy(extraShifter) let liquidProperties = try engine.block.findAllProperties(liquid) print("Liquid effect properties: \(liquidProperties)") } ``` Apply distortion effects to warp, shift, and transform images and videos for dynamic artistic visuals using CE.SDK's effect system. ![A grid showing the same photo unaltered alongside the liquid, mirror, shifter, radial pixel, and TV glitch distortion effects](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-distortion) Distortion effects differ from color filters in that they modify the geometry and spatial arrangement of pixels rather than their color values. CE.SDK provides several distortion effect types: liquid warping, mirror reflections, color channel shifting, radial pixelation, and TV glitch. Each effect offers configurable parameters to control the intensity and style of the distortion. This guide covers how to apply and configure each distortion effect programmatically, combine multiple effects on a single block, and manage the effect stack with the block API. To compare the effects side by side, the example places six copies of the same image in a grid and applies one effect to each — `liquidBlock`, `mirrorBlock`, and so on are the individual image cells. ## Check Effect Support Before applying distortion effects, verify the block supports them with `engine.block.supportsEffects`. Graphic blocks with image or video fills support effects, while scene blocks do not. ```swift highlight-distortion-checkSupport let canHaveEffects = try engine.block.supportsEffects(liquidBlock) print("Block supports effects: \(canHaveEffects)") ``` ## Apply Liquid Effect The liquid effect creates organic, flowing distortions that warp the image as if viewed through water. Create the effect with `engine.block.createEffect(.liquid)`, configure its properties with `engine.block.setFloat`, then attach it with `engine.block.appendEffect`. ```swift highlight-distortion-liquid let liquid = try engine.block.createEffect(.liquid) try engine.block.setFloat(liquid, property: "effect/liquid/amount", value: 0.5) try engine.block.setFloat(liquid, property: "effect/liquid/scale", value: 1.0) try engine.block.appendEffect(liquidBlock, effectID: liquid) ``` The liquid effect properties: - `effect/liquid/amount` (`0.0` to `1.0`) — Intensity of the warping. - `effect/liquid/scale` — Scale of the liquid pattern. - `effect/liquid/time` — Animation time offset for animated liquid distortions. ## Apply Mirror Effect The mirror effect reflects the image along a configurable side, creating symmetrical compositions. ```swift highlight-distortion-mirror let mirror = try engine.block.createEffect(.mirror) try engine.block.setInt(mirror, property: "effect/mirror/side", value: 0) try engine.block.appendEffect(mirrorBlock, effectID: mirror) ``` The `effect/mirror/side` property is an integer: `0` (Left), `1` (Right), `2` (Top), or `3` (Bottom). Set it with `engine.block.setInt`. ## Apply Shifter Effect The shifter effect displaces color channels at an angle, creating chromatic aberration commonly seen in glitch art and retro visuals. ```swift highlight-distortion-shifter let shifter = try engine.block.createEffect(.shifter) try engine.block.setFloat(shifter, property: "effect/shifter/amount", value: 0.3) try engine.block.setFloat(shifter, property: "effect/shifter/angle", value: 0.785) try engine.block.appendEffect(shifterBlock, effectID: shifter) ``` The shifter effect properties: - `effect/shifter/amount` (`0.0` to `1.0`) — Displacement distance. - `effect/shifter/angle` — Direction of the shift in radians. ## Apply Radial Pixel Effect The radial pixel effect pixelates the image in a circular pattern emanating from the center, useful for focus effects or stylized treatments. ```swift highlight-distortion-radialPixel let radialPixel = try engine.block.createEffect(.radialPixel) try engine.block.setFloat(radialPixel, property: "effect/radial_pixel/radius", value: 0.5) try engine.block.setFloat(radialPixel, property: "effect/radial_pixel/segments", value: 0.5) try engine.block.appendEffect(radialPixelBlock, effectID: radialPixel) ``` The radial pixel effect properties: - `effect/radial_pixel/radius` (up to `1.0`) — Radius of each row of pixels, relative to the image. - `effect/radial_pixel/segments` (up to `1.0`) — Proportional size of a pixel in each row. ## Apply TV Glitch Effect The TV glitch effect simulates analog television interference with horizontal distortion and rolling effects, popular for retro and digital aesthetics. ```swift highlight-distortion-tvGlitch let tvGlitch = try engine.block.createEffect(.tvGlitch) try engine.block.setFloat(tvGlitch, property: "effect/tv_glitch/distortion", value: 0.4) try engine.block.setFloat(tvGlitch, property: "effect/tv_glitch/distortion2", value: 0.2) try engine.block.setFloat(tvGlitch, property: "effect/tv_glitch/speed", value: 0.5) try engine.block.setFloat(tvGlitch, property: "effect/tv_glitch/rollSpeed", value: 0.5) try engine.block.appendEffect(tvGlitchBlock, effectID: tvGlitch) ``` The TV glitch effect properties: - `effect/tv_glitch/distortion` — Primary horizontal distortion intensity. - `effect/tv_glitch/distortion2` — Secondary distortion layer. - `effect/tv_glitch/speed` — Animation speed for the glitch effect. - `effect/tv_glitch/rollSpeed` — Vertical roll speed simulating signal sync issues. ## Combine Multiple Distortion Effects Stack multiple distortion effects on a single block. Here a shifter is added to `liquidBlock`, which already carries the liquid effect. Effects render in the order they appear in the block's effect list, from bottom to top, so the liquid warp is applied first and the shifter then displaces color channels on the already-warped result. Use `engine.block.appendEffect` to add an effect to the end of the list, or `engine.block.insertEffect` to place it at a specific index. ```swift highlight-distortion-combine let extraShifter = try engine.block.createEffect(.shifter) try engine.block.setFloat(extraShifter, property: "effect/shifter/amount", value: 0.2) try engine.block.appendEffect(liquidBlock, effectID: extraShifter) ``` ## List Applied Effects Retrieve every effect attached to a block with `engine.block.getEffects`. It returns an ordered array of effect IDs. ```swift highlight-distortion-getEffects let effects = try engine.block.getEffects(liquidBlock) print("Applied effects: \(effects.count)") ``` ## Enable and Disable Effects Toggle an effect on and off without removing it using `engine.block.setEffectEnabled`, and query its state with `engine.block.isEffectEnabled`. A disabled effect stays attached and keeps its parameters, but the engine skips it during rendering — useful for before/after comparisons or performance tuning. ```swift highlight-distortion-toggle try engine.block.setEffectEnabled(effectID: extraShifter, enabled: false) let shifterEnabled = try engine.block.isEffectEnabled(effectID: extraShifter) print("Shifter enabled: \(shifterEnabled)") ``` ## Remove Effects Remove an effect from a block by index with `engine.block.removeEffect`, then call `engine.block.destroy` on the removed effect to free its instance. ```swift highlight-distortion-remove try engine.block.removeEffect(liquidBlock, index: 1) try engine.block.destroy(extraShifter) ``` ## Discover Effect Properties Use `engine.block.findAllProperties` to discover every property available on an effect. The returned property paths work with `engine.block.setFloat`, `engine.block.setInt`, and `engine.block.setEnum`. ```swift highlight-distortion-properties let liquidProperties = try engine.block.findAllProperties(liquid) print("Liquid effect properties: \(liquidProperties)") ``` ## Troubleshooting ### Effect Not Visible Confirm the block supports effects with `supportsEffects` and that the effect is enabled with `isEffectEnabled`. Effects apply to graphic blocks with image or video fills, not to scene blocks. ### Unexpected Results Verify parameter values fall within their expected ranges. Many distortion parameters are normalized to `0.0`–`1.0`, but some use wider ranges — the TV glitch intensities and the shifter angle, for example. Values outside the expected range can produce extreme or unintended distortion. ### Performance Distortion effects are GPU-intensive. Limit the number of stacked effects on a single block, especially on mobile devices, and disable effects you aren't actively rendering. ## API Reference | Method | Description | |--------|-------------| | `engine.block.supportsEffects(_:)` | Check if a block supports effects | | `engine.block.createEffect(_:)` | Create a new effect instance from an `EffectType` | | `engine.block.appendEffect(_:effectID:)` | Add an effect to the end of a block's effect list | | `engine.block.insertEffect(_:effectID:index:)` | Insert an effect at a specific position | | `engine.block.getEffects(_:)` | Get all effects applied to a block | | `engine.block.removeEffect(_:index:)` | Remove an effect at a specific index | | `engine.block.setEffectEnabled(effectID:enabled:)` | Enable or disable an effect | | `engine.block.isEffectEnabled(effectID:)` | Check if an effect is enabled | | `engine.block.findAllProperties(_:)` | Discover all properties of an effect | | `engine.block.setFloat(_:property:value:)` | Set a float property value | | `engine.block.setInt(_:property:value:)` | Set an integer property value | | `engine.block.setEnum(_:property:value:)` | Set an enum property value | | `engine.block.destroy(_:)` | Destroy an effect instance to free memory | ## Available Distortion Effects | Effect | `EffectType` | Description | Key Properties | |--------|--------------|-------------|----------------| | Liquid | `.liquid` | Flowing, organic warping | `amount`, `scale`, `time` | | Mirror | `.mirror` | Reflection along a side | `side` (0=Left, 1=Right, 2=Top, 3=Bottom) | | Shifter | `.shifter` | Chromatic aberration | `amount`, `angle` | | Radial Pixel | `.radialPixel` | Circular pixelation | `radius`, `segments` | | TV Glitch | `.tvGlitch` | Analog TV interference | `distortion`, `distortion2`, `speed`, `rollSpeed` | ## Next Steps - [Apply Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) — Learn the foundational effect APIs. - [Blur Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/blur-71d642/) — Apply blur techniques for depth and focus 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: "Duotone" description: "Apply duotone effects to images with the CE.SDK Engine, mapping tones to two colors for stylized, brand-consistent visuals." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/duotone-831fc5/" --- > 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/) > [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) > [Duotone](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/duotone-831fc5/) --- ```swift file=@cesdk_swift_examples/engine-guides-duotone/Duotone.swift reference-only import Foundation import IMGLYEngine @MainActor func duotone(engine: Engine) async throws { // Demo scaffolding: a wide page that holds three image blocks, each showing // the same photo under a different duotone treatment. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1100) try engine.block.setHeight(page, value: 380) try engine.block.appendChild(to: scene, child: page) let baseURL = try engine.guidesBaseURL let imageURI = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") // Demo scaffolding: the first image block, styled by the preset section below. let presetImage = try engine.block.create(.graphic) try engine.block.setShape(presetImage, shape: engine.block.createShape(.rect)) try engine.block.setWidth(presetImage, value: 340) try engine.block.setHeight(presetImage, value: 320) try engine.block.setPositionX(presetImage, value: 20) try engine.block.setPositionY(presetImage, value: 30) let presetFill = try engine.block.createFill(.image) try engine.block.setURL(presetFill, property: "fill/image/imageFileURI", value: imageURI) try engine.block.setFill(presetImage, fill: presetFill) try engine.block.appendChild(to: page, child: presetImage) let canApplyEffects = try engine.block.supportsEffects(presetImage) guard canApplyEffects else { return } let filterSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.filter/content.json"), matcher: ["ly.img.filter.duotone.*"], ) let presetResult = try await engine.asset.findAssets( sourceID: filterSourceID, query: AssetQueryData(query: nil, page: 0, perPage: 10), ) let duotonePresets = presetResult.assets let presetEffect = try engine.block.createEffect(.duotoneFilter) if let preset = duotonePresets.first, let darkHex = preset.meta?["darkColor"], let lightHex = preset.meta?["lightColor"] { try engine.block.setColor(presetEffect, property: "effect/duotone_filter/darkColor", color: hexToRGBA(darkHex)) try engine.block.setColor(presetEffect, property: "effect/duotone_filter/lightColor", color: hexToRGBA(lightHex)) try engine.block.setFloat(presetEffect, property: "effect/duotone_filter/intensity", value: 0.9) } try engine.block.appendEffect(presetImage, effectID: presetEffect) try await engine.captureGuide(page, label: "after-preset") // Demo scaffolding: a second image block for the custom-color treatment. let customImage = try engine.block.create(.graphic) try engine.block.setShape(customImage, shape: engine.block.createShape(.rect)) try engine.block.setWidth(customImage, value: 340) try engine.block.setHeight(customImage, value: 320) try engine.block.setPositionX(customImage, value: 380) try engine.block.setPositionY(customImage, value: 30) let customFill = try engine.block.createFill(.image) try engine.block.setURL(customFill, property: "fill/image/imageFileURI", value: imageURI) try engine.block.setFill(customImage, fill: customFill) try engine.block.appendChild(to: page, child: customImage) let customEffect = try engine.block.createEffect(.duotoneFilter) // Dark color maps to shadows, light color maps to highlights. try engine.block.setColor( customEffect, property: "effect/duotone_filter/darkColor", color: .rgba(r: 0.1, g: 0.15, b: 0.3, a: 1.0), ) try engine.block.setColor( customEffect, property: "effect/duotone_filter/lightColor", color: .rgba(r: 0.95, g: 0.9, b: 0.8, a: 1.0), ) try engine.block.setFloat(customEffect, property: "effect/duotone_filter/intensity", value: 0.85) try engine.block.appendEffect(customImage, effectID: customEffect) try await engine.captureGuide(page, label: "after-custom") // Demo scaffolding: a third image block for the combined-effects treatment. let combinedImage = try engine.block.create(.graphic) try engine.block.setShape(combinedImage, shape: engine.block.createShape(.rect)) try engine.block.setWidth(combinedImage, value: 340) try engine.block.setHeight(combinedImage, value: 320) try engine.block.setPositionX(combinedImage, value: 740) try engine.block.setPositionY(combinedImage, value: 30) let combinedFill = try engine.block.createFill(.image) try engine.block.setURL(combinedFill, property: "fill/image/imageFileURI", value: imageURI) try engine.block.setFill(combinedImage, fill: combinedFill) try engine.block.appendChild(to: page, child: combinedImage) // Adjustments run first, then duotone maps the adjusted tones. let adjustments = try engine.block.createEffect(.adjustments) try engine.block.setFloat(adjustments, property: "effect/adjustments/brightness", value: 0.1) try engine.block.setFloat(adjustments, property: "effect/adjustments/contrast", value: 0.15) try engine.block.appendEffect(combinedImage, effectID: adjustments) let combinedDuotone = try engine.block.createEffect(.duotoneFilter) try engine.block.setColor( combinedDuotone, property: "effect/duotone_filter/darkColor", color: .rgba(r: 0.2, g: 0.1, b: 0.3, a: 1.0), ) try engine.block.setColor( combinedDuotone, property: "effect/duotone_filter/lightColor", color: .rgba(r: 1.0, g: 0.85, b: 0.7, a: 1.0), ) try engine.block.setFloat(combinedDuotone, property: "effect/duotone_filter/intensity", value: 0.75) try engine.block.appendEffect(combinedImage, effectID: combinedDuotone) try await engine.captureGuide(page, label: "hero") let appliedEffects = try engine.block.getEffects(presetImage) print("Image has \(appliedEffects.count) effect(s) applied") if let firstEffect = appliedEffects.first { try engine.block.setEffectEnabled(effectID: firstEffect, enabled: false) let isEnabled = try engine.block.isEffectEnabled(effectID: firstEffect) print("Effect enabled: \(isEnabled)") try engine.block.setEffectEnabled(effectID: firstEffect, enabled: true) } let customEffects = try engine.block.getEffects(customImage) if let effectToRemove = customEffects.first { // Detach the effect from the block's stack, then destroy it to free its resources. try engine.block.removeEffect(customImage, index: 0) try engine.block.destroy(effectToRemove) } } /// Converts a `#rrggbb` (or `rrggbb`) hex string to a `Color` with channels in the 0–1 range. private func hexToRGBA(_ hex: String) -> Color { let cleaned = hex.hasPrefix("#") ? String(hex.dropFirst()) : hex let value = UInt64(cleaned, radix: 16) ?? 0 let red = Float((value >> 16) & 0xFF) / 255 let green = Float((value >> 8) & 0xFF) / 255 let blue = Float(value & 0xFF) / 255 return .rgba(r: red, g: green, b: blue, a: 1.0) } ``` Apply duotone effects to images with the CE.SDK Engine, mapping image tones to two colors for stylized visuals, vintage aesthetics, and brand-consistent treatments. ![The same photo under three duotone treatments: a library preset, a custom navy-to-cream pair, and a combined adjustments-plus-duotone treatment](./assets/swift-based.hero.webp) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-duotone) Duotone is a color effect that maps image brightness to two colors: a dark color for shadows and a light color for highlights. The result is a striking two-tone image where the original colors are replaced by gradations between your chosen pair. This guide covers applying duotone presets from the asset library, creating custom color combinations, combining duotone with other effects, and managing applied effects. ## Understanding Duotone Unlike filters that tint or shift colors, duotone remaps the tonal range of an image. The effect reads each pixel's brightness and assigns a color based on where it falls between black and white: - **Dark tones** (shadows, blacks) adopt the dark color. - **Light tones** (highlights, whites) adopt the light color. - **Midtones** blend between the two colors. This produces a consistent palette regardless of the original colors, which makes duotone effective for: - **Brand consistency** — apply your brand colors across diverse imagery. - **Visual cohesion** — unify photos from different sources in one design. - **Vintage aesthetics** — recreate classic print techniques like cyanotype or sepia. - **Bold statements** — create eye-catching visuals for social media or marketing. An `intensity` value from `-1.0` to `1.0` controls the tonal balance: negative values push the mapping toward the dark color, positive values toward the light color, and `0.0` maps tones evenly between the two. The output is always a two-color image — intensity shifts the balance rather than fading back toward the original colors. ## Using the Built-in Duotone UI On iOS, CE.SDK's prebuilt editor lets users apply duotone interactively: selecting an image block surfaces a filters section in the inspector, where they can browse duotone presets, apply one with a tap, and adjust its intensity. The rest of this guide covers the Engine API behind that experience, which applies duotone programmatically on every Apple platform. See [Apply Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) for the unified effects system the filters section builds on. ## Check Effect Support Not all blocks accept effects — graphic blocks and pages do, but the scene does not. Verify support with `supportsEffects(_:)` before applying anything. Duotone is most useful on graphic blocks with an image or video fill, since it remaps the tones of visible pixel content. ```swift highlight-duotone-supportsEffects let canApplyEffects = try engine.block.supportsEffects(presetImage) guard canApplyEffects else { return } ``` Applying an effect to a block that doesn't support effects throws, so gate the call on `supportsEffects(_:)`. ## Applying Duotone Presets CE.SDK ships a library of professionally designed duotone presets. Each preset defines a dark/light color pair as hex strings in its metadata. ### Query Built-in Presets Register the bundled filter source and query it with the Asset API. The `matcher` argument limits the registered assets to the duotone presets, so the query returns only duotone entries rather than the full filter library. ```swift highlight-duotone-queryPresets let filterSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.filter/content.json"), matcher: ["ly.img.filter.duotone.*"], ) let presetResult = try await engine.asset.findAssets( sourceID: filterSourceID, query: AssetQueryData(query: nil, page: 0, perPage: 10), ) let duotonePresets = presetResult.assets ``` Each preset exposes `darkColor` and `lightColor` in `meta` as hex strings. Convert them to the engine's `Color` type, whose channels run from `0.0` to `1.0`, before applying them. ### Create the Effect Block Create a duotone effect with `createEffect(_:)`, passing the `.duotoneFilter` effect type. The effect is a standalone block you configure and then attach to an image. ```swift highlight-duotone-createEffect let presetEffect = try engine.block.createEffect(.duotoneFilter) ``` ### Convert Preset Colors Preset colors arrive as hex strings, so convert them to `Color` values. This helper parses a `#rrggbb` string into a `.rgba` color with channels normalized to the `0.0–1.0` range. ```swift highlight-duotone-hexHelper /// Converts a `#rrggbb` (or `rrggbb`) hex string to a `Color` with channels in the 0–1 range. private func hexToRGBA(_ hex: String) -> Color { let cleaned = hex.hasPrefix("#") ? String(hex.dropFirst()) : hex let value = UInt64(cleaned, radix: 16) ?? 0 let red = Float((value >> 16) & 0xFF) / 255 let green = Float((value >> 8) & 0xFF) / 255 let blue = Float(value & 0xFF) / 255 return .rgba(r: red, g: green, b: blue, a: 1.0) } ``` Apply the converted colors to the effect with `setColor(_:property:color:)`, and set `intensity` with `setFloat(_:property:value:)`. `intensity` ranges from `-1.0` to `1.0` (default `0.0`): negative values emphasize the dark color, positive values emphasize the light color, and `0.0` keeps the two balanced. ```swift highlight-duotone-applyPreset if let preset = duotonePresets.first, let darkHex = preset.meta?["darkColor"], let lightHex = preset.meta?["lightColor"] { try engine.block.setColor(presetEffect, property: "effect/duotone_filter/darkColor", color: hexToRGBA(darkHex)) try engine.block.setColor(presetEffect, property: "effect/duotone_filter/lightColor", color: hexToRGBA(lightHex)) try engine.block.setFloat(presetEffect, property: "effect/duotone_filter/intensity", value: 0.9) } ``` ### Append the Effect Attach the configured effect to the image block. The duotone takes effect as soon as it joins the block's effect stack. ```swift highlight-duotone-appendPreset try engine.block.appendEffect(presetImage, effectID: presetEffect) ``` ## Creating Custom Colors For brand-specific treatments, define your own pair directly with `setColor(_:property:color:)`. The dark color maps to shadows and the light color to highlights. ```swift highlight-duotone-customColors let customEffect = try engine.block.createEffect(.duotoneFilter) // Dark color maps to shadows, light color maps to highlights. try engine.block.setColor( customEffect, property: "effect/duotone_filter/darkColor", color: .rgba(r: 0.1, g: 0.15, b: 0.3, a: 1.0), ) try engine.block.setColor( customEffect, property: "effect/duotone_filter/lightColor", color: .rgba(r: 0.95, g: 0.9, b: 0.8, a: 1.0), ) try engine.block.setFloat(customEffect, property: "effect/duotone_filter/intensity", value: 0.85) try engine.block.appendEffect(customImage, effectID: customEffect) ``` ### Choosing Effective Color Pairs The relationship between the dark and light colors determines the final look: | Color Relationship | Visual Effect | Example Use Case | | --- | --- | --- | | **High contrast** | Bold, graphic look | Social media, posters | | **Low contrast** | Subtle, sophisticated | Editorial, luxury brands | | **Warm to cool** | Dynamic temperature shift | Lifestyle, fashion | | **Monochromatic** | Tinted photography style | Vintage, noir aesthetic | Classic combinations to try: - **Cyanotype**: deep blue to light cyan - **Sepia**: dark brown to cream - **Corporate**: navy to silver > **Tip:** Start from your brand palette. Use a primary brand color as the light color for highlights, paired with a darker complementary shade for shadows. ## Combining with Other Effects Effects apply in stack order, so you can pair duotone with adjustments, blur, or other effects. Here, brightness and contrast adjustments run first, then duotone maps the adjusted tones. ```swift highlight-duotone-combineEffects // Adjustments run first, then duotone maps the adjusted tones. let adjustments = try engine.block.createEffect(.adjustments) try engine.block.setFloat(adjustments, property: "effect/adjustments/brightness", value: 0.1) try engine.block.setFloat(adjustments, property: "effect/adjustments/contrast", value: 0.15) try engine.block.appendEffect(combinedImage, effectID: adjustments) let combinedDuotone = try engine.block.createEffect(.duotoneFilter) try engine.block.setColor( combinedDuotone, property: "effect/duotone_filter/darkColor", color: .rgba(r: 0.2, g: 0.1, b: 0.3, a: 1.0), ) try engine.block.setColor( combinedDuotone, property: "effect/duotone_filter/lightColor", color: .rgba(r: 1.0, g: 0.85, b: 0.7, a: 1.0), ) try engine.block.setFloat(combinedDuotone, property: "effect/duotone_filter/intensity", value: 0.75) try engine.block.appendEffect(combinedImage, effectID: combinedDuotone) ``` Reversing the order — duotone first, then adjustments — produces a different result, because the adjustments would then operate on the duotone's output rather than the original image. ## Managing Duotone Effects Once effects are applied, you can list, toggle, and remove them. ### List Applied Effects Retrieve the ordered list of effect IDs attached to a block with `getEffects(_:)`. ```swift highlight-duotone-listEffects let appliedEffects = try engine.block.getEffects(presetImage) print("Image has \(appliedEffects.count) effect(s) applied") ``` ### Toggle Effect Visibility Disable an effect without removing it using `setEffectEnabled(effectID:enabled:)`, and query its state with `isEffectEnabled(effectID:)`. Disabled effects are skipped when the block renders. ```swift highlight-duotone-toggleEffects if let firstEffect = appliedEffects.first { try engine.block.setEffectEnabled(effectID: firstEffect, enabled: false) let isEnabled = try engine.block.isEffectEnabled(effectID: firstEffect) print("Effect enabled: \(isEnabled)") try engine.block.setEffectEnabled(effectID: firstEffect, enabled: true) } ``` ### Remove and Destroy Effects Detach an effect from a block by its index with `removeEffect(_:index:)`. Effects are independent blocks that persist after removal, so destroy the detached effect with `destroy(_:)` to free its resources. ```swift highlight-duotone-removeEffect let customEffects = try engine.block.getEffects(customImage) if let effectToRemove = customEffects.first { // Detach the effect from the block's stack, then destroy it to free its resources. try engine.block.removeEffect(customImage, index: 0) try engine.block.destroy(effectToRemove) } ``` ## Troubleshooting ### Duotone Not Visible Confirm the block supports effects with `supportsEffects(_:)` — graphic blocks and pages do, the scene does not. Duotone also needs visible image or video content to remap; a solid color fill maps to a single flat tone. ### Colors Look Wrong `Color.rgba` channels run from `0.0` to `1.0`, not `0` to `255`. Use `.rgba(r: 0.5, g: 0.5, b: 0.5, a: 1.0)` rather than raw byte values. ### Duotone Leans Too Dark or Too Light The `intensity` property controls the tonal balance, not opacity — the image is always fully duotoned. If the result leans too dark, raise `intensity` toward `1.0` to emphasize the light color; if it leans too light, lower it toward `-1.0` to emphasize the dark color. `0.0` maps tones evenly. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addLocalAssetSourceFromJSON(_:matcher:)` | Register a local asset source, optionally filtered by ID pattern | | `engine.asset.findAssets(sourceID:query:)` | Query assets from a registered source | | `engine.block.supportsEffects(_:)` | Return whether a block can have effects applied | | `engine.block.createEffect(_:)` | Create an effect block of the given `EffectType` | | `engine.block.setColor(_:property:color:)` | Set a color property on a block | | `engine.block.setFloat(_:property:value:)` | Set a float property on a block | | `engine.block.appendEffect(_:effectID:)` | Append an effect to a block's effect stack | | `engine.block.getEffects(_:)` | Return the ordered effect IDs applied to a block | | `engine.block.setEffectEnabled(effectID:enabled:)` | Enable or disable an effect | | `engine.block.isEffectEnabled(effectID:)` | Return whether an effect is enabled | | `engine.block.removeEffect(_:index:)` | Remove an effect at the given index | | `engine.block.destroy(_:)` | Destroy a block and free its resources | ### Properties | Property | Type | Description | | --- | --- | --- | | `effect/duotone_filter/darkColor` | Color | Color applied to shadows and dark tones | | `effect/duotone_filter/lightColor` | Color | Color applied to highlights and light tones | | `effect/duotone_filter/intensity` | Float | Tonal balance from `-1.0` to `1.0` (default `0.0`); negative emphasizes the dark color, positive emphasizes the light color | ## Next Steps - [Apply Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) — Learn about the unified effects system. - [Blur Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/blur-71d642/) — Apply blur for depth and focus. - [Create a Custom LUT Filter](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/create-custom-lut-filter-6e3f49/) — Build custom color grading filters. --- ## 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: "Enhance visual elements with filters and effects such as blur, duotone, LUTs, and chroma keying." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/overview-299b15/" --- > 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/) > [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/overview-299b15/) --- In CreativeEditor SDK (CE.SDK), *filters* and *effects* refer to visual modifications that enhance or transform the appearance of design elements. Filters typically adjust an element’s overall color or tone, while effects add specific visual treatments like blur, sharpness, or distortion. You can apply both filters and effects through the user interface or programmatically using the CE.SDK API. They allow you to refine the look of images, videos, and graphic elements in your designs with precision and flexibility. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) --- ## 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 Filters and Effects" description: "Discover the filters and effects available in CE.SDK and check whether a block supports them using the Swift Engine API." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/support-a666dd/" --- > 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/) > [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) > [Supported Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/support-a666dd/) --- ```swift file=@cesdk_swift_examples/engine-guides-supported-filters-and-effects/SupportedFiltersAndEffects.swift reference-only import Foundation import IMGLYEngine @MainActor func supportedFiltersAndEffects(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: 600) try engine.block.setHeight(imageBlock, value: 450) try engine.block.setPositionX(imageBlock, value: 100) try engine.block.setPositionY(imageBlock, value: 75) 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) let canHaveEffects = try engine.block.supportsEffects(imageBlock) print("Block supports effects: \(canHaveEffects)") let duotoneEffect = try engine.block.createEffect(.duotoneFilter) try engine.block.appendEffect(imageBlock, effectID: duotoneEffect) try engine.block.setColor( duotoneEffect, property: "effect/duotone_filter/darkColor", color: .rgba(r: 0.02, g: 0.04, b: 0.12, a: 1.0), ) try engine.block.setColor( duotoneEffect, property: "effect/duotone_filter/lightColor", color: .rgba(r: 0.5, g: 0.7, b: 1.0, a: 1.0), ) try engine.block.setFloat(duotoneEffect, property: "effect/duotone_filter/intensity", value: 0.8) let appliedEffects = try engine.block.getEffects(imageBlock) print("Number of applied effects: \(appliedEffects.count)") for (index, effect) in appliedEffects.enumerated() { let effectType = try engine.block.getType(effect) print("Effect \(index): \(effectType)") } } ``` Discover all available filters and effects in CE.SDK and learn how to check if a block supports them. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-supported-filters-and-effects) CE.SDK provides 22 built-in effect types for visual transformations including color adjustments, blur effects, artistic filters, and distortion effects. This reference guide shows how to check effect support and add effects programmatically, followed by detailed property tables for each effect type. This guide covers checking effect support on blocks, adding effects programmatically, and the complete list of available effect types with their properties. For detailed tutorials on configuring and combining multiple effects, see the [Apply Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) guide. ## Check Effect Support Before applying effects to a block, verify whether it supports them using `supportsEffects(_:)`. Not all block types can have effects applied. The snippet below creates a graphic block with an image fill, then checks support. ```swift highlight-supportedEffects-checkSupport let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(imageBlock, value: 600) try engine.block.setHeight(imageBlock, value: 450) try engine.block.setPositionX(imageBlock, value: 100) try engine.block.setPositionY(imageBlock, value: 75) 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) let canHaveEffects = try engine.block.supportsEffects(imageBlock) print("Block supports effects: \(canHaveEffects)") ``` Effect support is available for: - **Graphic blocks** — including those displaying images, videos, shapes, or solid colors - **Page blocks** — to apply an effect to the whole page Other block types, such as groups, return `false`. ## Add an Effect Create an effect with `createEffect(_:)` using an `EffectType` value, then attach it to a block's effect stack with `appendEffect(_:effectID:)`. ```swift highlight-supportedEffects-addEffect let duotoneEffect = try engine.block.createEffect(.duotoneFilter) try engine.block.appendEffect(imageBlock, effectID: duotoneEffect) ``` ## Configure Effect Properties Configure effect parameters using the typed setter methods. Property paths follow the format `effect/{effect-type}/{property-name}`. ```swift highlight-supportedEffects-configure try engine.block.setColor( duotoneEffect, property: "effect/duotone_filter/darkColor", color: .rgba(r: 0.02, g: 0.04, b: 0.12, a: 1.0), ) try engine.block.setColor( duotoneEffect, property: "effect/duotone_filter/lightColor", color: .rgba(r: 0.5, g: 0.7, b: 1.0, a: 1.0), ) try engine.block.setFloat(duotoneEffect, property: "effect/duotone_filter/intensity", value: 0.8) ``` CE.SDK provides typed setter methods for different parameter types: - **`setFloat(_:property:value:)`** - For intensity, amount, and decimal values - **`setInt(_:property:value:)`** - For discrete values like pixel sizes - **`setString(_:property:value:)`** - For file URIs (LUT files) - **`setBool(_:property:value:)`** - For enabling or disabling features - **`setColor(_:property:color:)`** - For color values ## Retrieve Applied Effects Use `getEffects(_:)` to retrieve all effects applied to a block, in the order they are applied. ```swift highlight-supportedEffects-getEffects let appliedEffects = try engine.block.getEffects(imageBlock) print("Number of applied effects: \(appliedEffects.count)") for (index, effect) in appliedEffects.enumerated() { let effectType = try engine.block.getType(effect) print("Effect \(index): \(effectType)") } ``` ## Effects The following tables document all available effect types and their configurable properties. ## Adjustments Type An effect block for basic image adjustments. This section describes the properties available for the **Adjustments Type** (`//ly.img.ubq/effect/adjustments`) block type. | Property | Type | Default | Description | | -------------------------------- | ------- | ------- | ------------------------------------ | | `effect/adjustments/blacks` | `Float` | `0` | Adjustment of only the blacks. | | `effect/adjustments/brightness` | `Float` | `0` | Adjustment of the brightness. | | `effect/adjustments/clarity` | `Float` | `0` | Adjustment of the detail. | | `effect/adjustments/contrast` | `Float` | `0` | Adjustment of the contrast. | | `effect/adjustments/exposure` | `Float` | `0` | Adjustment of the exposure. | | `effect/adjustments/gamma` | `Float` | `0` | Gamma correction, non-linear. | | `effect/adjustments/highlights` | `Float` | `0` | Adjustment of only the highlights. | | `effect/adjustments/saturation` | `Float` | `0` | Adjustment of the saturation. | | `effect/adjustments/shadows` | `Float` | `0` | Adjustment of only the shadows. | | `effect/adjustments/sharpness` | `Float` | `0` | Adjustment of the sharpness. | | `effect/adjustments/temperature` | `Float` | `0` | Adjustment of the color temperature. | | `effect/adjustments/whites` | `Float` | `0` | Adjustment of only the whites. | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | ## Cross Cut Type An effect that distorts the image with horizontal slices. This section describes the properties available for the **Cross Cut Type** (`//ly.img.ubq/effect/cross_cut`) block type. | Property | Type | Default | Description | | ------------------------- | ------- | ------- | ------------------------------ | | `effect/cross_cut/offset` | `Float` | `0.07` | Horizontal offset per slice. | | `effect/cross_cut/slices` | `Float` | `5` | Number of horizontal slices. | | `effect/cross_cut/speedV` | `Float` | `0.5` | Vertical slice position. | | `effect/cross_cut/time` | `Float` | `1` | Randomness input. | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | ## Dot Pattern Type An effect that displays the image using a dot matrix. This section describes the properties available for the **Dot Pattern Type** (`//ly.img.ubq/effect/dot_pattern`) block type. | Property | Type | Default | Description | | ------------------------- | ------- | ------- | ------------------------------ | | `effect/dot_pattern/blur` | `Float` | `0.3` | Global blur. | | `effect/dot_pattern/dots` | `Float` | `30` | Number of dots. | | `effect/dot_pattern/size` | `Float` | `0.5` | Size of an individual dot. | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | ## Duotone Filter Type An effect that applies a two-tone color mapping. This section describes the properties available for the **Duotone Filter Type** (`//ly.img.ubq/effect/duotone_filter`) block type. | Property | Type | Default | Description | | ---------------------------------- | ------- | --------------------------- | --------------------------------------------------------------------------------- | | `effect/duotone_filter/darkColor` | `Color` | `{"r":0,"g":0,"b":0,"a":0}` | The darker of the two colors. Negative filter intensities emphasize this color. | | `effect/duotone_filter/intensity` | `Float` | `0` | The mixing weight of the two colors in the range \[-1, 1]. | | `effect/duotone_filter/lightColor` | `Color` | `{"r":0,"g":0,"b":0,"a":0}` | The brighter of the two colors. Positive filter intensities emphasize this color. | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | ## Extrude Blur Type An effect that applies a radial extrude blur. This section describes the properties available for the **Extrude Blur Type** (`//ly.img.ubq/effect/extrude_blur`) block type. | Property | Type | Default | Description | | ---------------------------- | ------- | ------- | ------------------------------ | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/extrude_blur/amount` | `Float` | `0.2` | Blur intensity. | ## Glow Type An effect that applies an artificial glow. This section describes the properties available for the **Glow Type** (`//ly.img.ubq/effect/glow`) block type. | Property | Type | Default | Description | | ---------------------- | ------- | ------- | ------------------------------ | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/glow/amount` | `Float` | `0.5` | Glow brightness. | | `effect/glow/darkness` | `Float` | `0.3` | Glow darkness. | | `effect/glow/size` | `Float` | `4` | Intensity of the glow. | ## Green Screen Type An effect that replaces a specific color with transparency. This section describes the properties available for the **Green Screen Type** (`//ly.img.ubq/effect/green_screen`) block type. | Property | Type | Default | Description | | -------------------------------- | ------- | --------------------------- | ---------------------------------------------------------------------------------------------------- | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/green_screen/colorMatch` | `Float` | `0.4` | Threshold between the source color and the from color. | | `effect/green_screen/fromColor` | `Color` | `{"r":0,"g":1,"b":0,"a":1}` | The color to be replaced. | | `effect/green_screen/smoothness` | `Float` | `0.08` | Controls the rate at which the color transition increases when the similarity threshold is exceeded. | | `effect/green_screen/spill` | `Float` | `0` | Controls the desaturation of the source color to reduce color spill. | ## Half Tone Type An effect that overlays a halftone pattern. This section describes the properties available for the **Half Tone Type** (`//ly.img.ubq/effect/half_tone`) block type. | Property | Type | Default | Description | | ------------------------ | ------- | ------- | ------------------------------ | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/half_tone/angle` | `Float` | `0` | Angle of pattern. | | `effect/half_tone/scale` | `Float` | `0.5` | Scale of pattern. | ## Linocut Type An effect that overlays a linocut pattern. This section describes the properties available for the **Linocut Type** (`//ly.img.ubq/effect/linocut`) block type. | Property | Type | Default | Description | | ---------------------- | ------- | ------- | ------------------------------ | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/linocut/scale` | `Float` | `0.5` | Scale of pattern. | ## Liquid Type An effect that applies a liquefy distortion. This section describes the properties available for the **Liquid Type** (`//ly.img.ubq/effect/liquid`) block type. | Property | Type | Default | Description | | ---------------------- | ------- | ------- | ------------------------------- | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/liquid/amount` | `Float` | `0.06` | Severity of the applied effect. | | `effect/liquid/scale` | `Float` | `0.62` | Global scale. | | `effect/liquid/time` | `Float` | `0.5` | Continuous randomness input. | ## Lut Filter Type An effect that applies a color lookup table (LUT). This section describes the properties available for the **Lut Filter Type** (`//ly.img.ubq/effect/lut_filter`) block type. | Property | Type | Default | Description | | --------------------------------------- | -------- | ------- | ---------------------------------------------------------- | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/lut_filter/horizontalTileCount` | `Int` | `5` | The horizontal number of tiles contained in the LUT image. | | `effect/lut_filter/intensity` | `Float` | `1` | A value in the range of \[0, 1]. Defaults to 1.0. | | `effect/lut_filter/lutFileURI` | `String` | `""` | The URI to a LUT PNG file. | | `effect/lut_filter/verticalTileCount` | `Int` | `5` | The vertical number of tiles contained in the LUT image. | ## Mirror Type An effect that mirrors the image along a central axis. This section describes the properties available for the **Mirror Type** (`//ly.img.ubq/effect/mirror`) block type. | Property | Type | Default | Description | | -------------------- | ------ | ------- | ------------------------------ | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/mirror/side` | `Int` | `1` | Axis to mirror along. | ## Outliner Type An effect that highlights the outlines in an image. This section describes the properties available for the **Outliner Type** (`//ly.img.ubq/effect/outliner`) block type. | Property | Type | Default | Description | | ----------------------------- | ------- | ------- | -------------------------------------------- | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/outliner/amount` | `Float` | `0.5` | Intensity of edge highlighting. | | `effect/outliner/passthrough` | `Float` | `0.5` | Visibility of input image in non-edge areas. | ## Pixelize Type An effect that pixelizes the image. This section describes the properties available for the **Pixelize Type** (`//ly.img.ubq/effect/pixelize`) block type. | Property | Type | Default | Description | | ------------------------------------- | ------ | ------- | ----------------------------------- | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/pixelize/horizontalPixelSize` | `Int` | `20` | The number of pixels on the x-axis. | | `effect/pixelize/verticalPixelSize` | `Int` | `20` | The number of pixels on the y-axis. | ## Posterize Type An effect that reduces the number of colors in the image. This section describes the properties available for the **Posterize Type** (`//ly.img.ubq/effect/posterize`) block type. | Property | Type | Default | Description | | ------------------------- | ------- | ------- | ------------------------------ | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/posterize/levels` | `Float` | `3` | Number of color levels. | ## Radial Pixel Type An effect that reduces the image into radial pixel rows. This section describes the properties available for the **Radial Pixel Type** (`//ly.img.ubq/effect/radial_pixel`) block type. | Property | Type | Default | Description | | ------------------------------ | ------- | ------- | ------------------------------------------------------------- | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/radial_pixel/radius` | `Float` | `0.1` | Radius of an individual row of pixels, relative to the image. | | `effect/radial_pixel/segments` | `Float` | `0.01` | Proportional size of a pixel in each row. | ## Recolor Type An effect that replaces one color with another. This section describes the properties available for the **Recolor Type** (`//ly.img.ubq/effect/recolor`) block type. | Property | Type | Default | Description | | -------------------------------- | ------- | --------------------------- | ---------------------------------------------------------------------------------------------------- | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/recolor/brightnessMatch` | `Float` | `1` | Affects the weight of brightness when calculating color similarity. | | `effect/recolor/colorMatch` | `Float` | `0.4` | Threshold between the source color and the from color. | | `effect/recolor/fromColor` | `Color` | `{"r":1,"g":1,"b":1,"a":1}` | The color to be replaced. | | `effect/recolor/smoothness` | `Float` | `0.08` | Controls the rate at which the color transition increases when the similarity threshold is exceeded. | | `effect/recolor/toColor` | `Color` | `{"r":0,"g":0,"b":1,"a":1}` | The color to replace with. | ## Sharpie Type Cartoon-like effect. This section describes the properties available for the **Sharpie Type** (`//ly.img.ubq/effect/sharpie`) block type. | Property | Type | Default | Description | | ---------------- | ------ | ------- | ------------------------------ | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | ## Shifter Type An effect that shifts individual color channels. This section describes the properties available for the **Shifter Type** (`//ly.img.ubq/effect/shifter`) block type. | Property | Type | Default | Description | | ----------------------- | ------- | ------- | ------------------------------ | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/shifter/amount` | `Float` | `0.05` | Intensity of the shift. | | `effect/shifter/angle` | `Float` | `0.3` | Shift direction. | ## Tilt Shift Type An effect that applies a tilt-shift blur. This section describes the properties available for the **Tilt Shift Type** (`//ly.img.ubq/effect/tilt_shift`) block type. | Property | Type | Default | Description | | ---------------------------- | ------- | ------- | ------------------------------ | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/tilt_shift/amount` | `Float` | `0.016` | Blur intensity. | | `effect/tilt_shift/position` | `Float` | `0.4` | Horizontal position in image. | ## Tv Glitch Type An effect that mimics TV banding and distortion. This section describes the properties available for the **Tv Glitch Type** (`//ly.img.ubq/effect/tv_glitch`) block type. | Property | Type | Default | Description | | ------------------------------ | ------- | ------- | ---------------------------------- | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/tv_glitch/distortion` | `Float` | `3` | Rough horizontal distortion. | | `effect/tv_glitch/distortion2` | `Float` | `1` | Fine horizontal distortion. | | `effect/tv_glitch/rollSpeed` | `Float` | `1` | Vertical offset. | | `effect/tv_glitch/speed` | `Float` | `2` | Number of changes per time change. | ## Vignette Type An effect that adds a vignette (darkened corners). This section describes the properties available for the **Vignette Type** (`//ly.img.ubq/effect/vignette`) block type. | Property | Type | Default | Description | | -------------------------- | ------- | ------- | ------------------------------ | | `effect/enabled` | `Bool` | `true` | Whether the effect is enabled. | | `effect/vignette/darkness` | `Float` | `1` | Brightness of vignette. | | `effect/vignette/offset` | `Float` | `1` | Radial offset. | ## Next Steps - [Apply Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) - Learn how to configure, combine, and manage multiple 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: "Build with AI" description: "Give your AI coding assistant context about CE.SDK to generate accurate code and get instant answers." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/get-started/build-with-ai-k7m9p2/" --- > 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:** [Build with AI](https://img.ly/docs/cesdk/mac-catalyst/get-started/build-with-ai-k7m9p2/) --- Give your AI coding assistant full context about CE.SDK to generate accurate code and get instant answers. Choose the integration that fits your workflow. ## Choose Your Approach ### Want Everything in One Install? Install the **CE.SDK Plugin** into Claude Code to get bundled documentation skills, guided code generation, and an autonomous project scaffolder in a single command. [Install the Plugin](#broken-link-c0d3ag) ### Using an AI-Powered IDE? Connect your IDE to our **MCP Server** for real-time documentation search. Works with Claude Desktop, Cursor, VS Code Copilot, Windsurf and any MCP-compatible tool. [Connect MCP Server](https://img.ly/docs/cesdk/mac-catalyst/get-started/mcp-server-fde71c/) ### Using an AI Coding Assistant? Install our **Agent Skills** into Claude Code or the Vercel Skills CLI for bundled offline documentation, guided code generation, and autonomous project scaffolding across 10 Web frameworks. [Install Agent Skills](#broken-link-f7g8h9) ### Need Raw Documentation for AI? Download our **LLMs.txt** files to manually load CE.SDK documentation into any AI tool. Available as a compact index or full documentation bundle. [Download LLMs.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-txt-eb9cc5/) *** Looking to add AI **generation features** — image, video, audio, or text — into the editor for your own users? That's a different journey: see [AI Features](#broken-link-5aa356). --- ## Related Pages - [MCP Server](https://img.ly/docs/cesdk/mac-catalyst/get-started/mcp-server-fde71c/) - Connect AI assistants to CE.SDK documentation using the Model Context Protocol (MCP) server. - [LLMs.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-txt-eb9cc5/) - Our documentation is available in LLMs.txt format --- ## 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: "Quickstart" description: "Integrate the CE.SDK Engine into a Mac Catalyst app and host the canvas with SwiftUI or UIKit." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/get-started/mac-catalyst/quickstart-mcat0q/" --- > 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/) > [Quickstart Mac Catalyst](https://img.ly/docs/cesdk/mac-catalyst/get-started/mac-catalyst/quickstart-mcat0q/) --- ```swift file=@cesdk_swift_examples/engine-guides-integrate-with-swiftui/IntegrateWithSwiftUI.swift reference-only import IMGLYEngine import SwiftUI struct IntegrateWithSwiftUI: View { @State private var engine: Engine? var body: some View { Group { if let engine { Canvas(engine: engine) } else { ProgressView("Starting the engine…") } } .onAppear { guard engine == nil else { return } Task { do { let engine = try await Engine( license: secrets.licenseKey, // pass nil for evaluation mode with watermark userID: "", ) 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 text = try engine.block.create(.text) try engine.block.setString(text, property: "text/text", value: "Hello, CE.SDK!") try engine.block.setPositionX(text, value: 80) try engine.block.setPositionY(text, value: 260) try engine.block.setWidth(text, value: 640) try engine.block.appendChild(to: page, child: text) try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) self.engine = engine } catch { print("Engine setup failed: \(error)") } } } } } #if DEBUG // Live preview that boots a real engine so the file can be exercised inside // Xcode without launching a host app. Requires Xcode 15+. @available(iOS 17, macOS 14, *) #Preview { IntegrateWithSwiftUI() } #endif ``` ```swift file=@cesdk_swift_examples/engine-guides-integrate-with-uikit/IntegrateWithUIKit.swift reference-only #if os(iOS) import IMGLYEngine import MetalKit import UIKit final class IntegrateWithUIKit: UIViewController { private var engine: Engine? private lazy var canvas = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice()) private lazy var spinner: UIActivityIndicatorView = { let indicator = UIActivityIndicatorView(style: .large) indicator.translatesAutoresizingMaskIntoConstraints = false indicator.hidesWhenStopped = true return indicator }() override func viewDidLoad() { super.viewDidLoad() view.addSubview(canvas) canvas.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ canvas.leftAnchor.constraint(equalTo: view.leftAnchor), canvas.rightAnchor.constraint(equalTo: view.rightAnchor), canvas.topAnchor.constraint(equalTo: view.topAnchor), canvas.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) view.addSubview(spinner) NSLayoutConstraint.activate([ spinner.centerXAnchor.constraint(equalTo: view.centerXAnchor), spinner.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) spinner.startAnimating() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) guard engine == nil else { return } Task { do { let engine = try await Engine( context: .metalView(view: canvas), license: secrets.licenseKey, // pass nil for evaluation mode with watermark userID: "", ) engine.onAppear() 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 text = try engine.block.create(.text) try engine.block.setString(text, property: "text/text", value: "Hello, CE.SDK!") try engine.block.setPositionX(text, value: 80) try engine.block.setPositionY(text, value: 260) try engine.block.setWidth(text, value: 640) try engine.block.appendChild(to: page, child: text) try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) self.engine = engine spinner.stopAnimating() } catch { print("Engine setup failed: \(error)") } } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) engine?.onDisappear() } } #endif ``` This guide walks you through integrating the CE.SDK Engine into a brand-new Mac Catalyst app. Mac Catalyst runs your UIKit-based iOS app on the Mac, so you host the engine's canvas inside your own SwiftUI or UIKit view and drive it with the engine APIs — there is no prebuilt editor UI to drop in. > **Note:** The prebuilt editor and camera (`IMGLYEditor`, `IMGLYCamera`) build on iOS only — CE.SDK does not currently ship a packaged UI like the iOS `IMGLYUI` package for macOS or Mac Catalyst. On Mac Catalyst, `IMGLYEngine` is the module you integrate: initialize the engine, host its canvas, and build your own controls on top. See the [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) guide for the engine's capabilities and [Build Your Own UI](https://img.ly/docs/cesdk/mac-catalyst/user-interface/build-your-own-ui-fe7527/) for a complete custom-editor walkthrough. If you need a packaged UI on these platforms rather than building your own, [get in touch with us](https://img.ly/forms/contact-sales). > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$) ## Requirements To work with the SDK, you'll need: - A Mac running a recent version of [Xcode](https://developer.apple.com/xcode/) - A deployment target of macOS 11 or later (Mac Catalyst) - A valid **CE.SDK license key** ([Get a free trial](https://img.ly/forms/free-trial)) ## Creating a new Xcode Project **1.** Launch Xcode and use the `File` menu to select `New` -> `Project...`. **2.** Select the `iOS` tab, highlight the `App` template, and click `Next`. Mac Catalyst builds from an iOS app target, so you start from the iOS `App` template. **3.** Enter a product name and an organization identifier, and set the language to `Swift`. For the interface, choose `SwiftUI` or, for a UIKit app, `Storyboard` — which scaffolds a `UIViewController` (`ViewController.swift`) to build on. Match the hosting path you'll follow in the Host the Canvas section below, then click `Next`. **4.** Choose a location to save the project and click `Create`. ## Enable the Mac Catalyst Destination **1.** Select your project in the navigator, then select the app target. **2.** On the `General` tab, find **Supported Destinations** and click the `+` button. **3.** Choose **Mac (Mac Catalyst)** from the list. Xcode adds Mac Catalyst as a run destination alongside the iOS simulators. ## Add the CE.SDK Swift package **1.** With your Xcode project open, use the `File` menu to select `Add Package Dependencies...` **2.** Copy the following package URL and paste it into the search field at the top right of the dialog: https://github.com/imgly/IMGLYEngine-swift **3.** Once the package resolves, click `Add Package`. **4.** When Xcode presents the list of libraries, add the `IMGLYEngine` library to your app target, then click `Add Package`. > **Warning:** On Mac Catalyst, add the `IMGLYEngine-swift` package — not `IMGLYUI-swift`. The `IMGLYUI` package that powers the prebuilt editor and camera builds on iOS only, so it cannot be linked into a Mac Catalyst target. `IMGLYEngine` ships on iOS, macOS, and Mac Catalyst. ## Host the Canvas The engine renders into a Metal view. Initialize the engine with `try await Engine(...)`, then place its canvas in your view hierarchy. Pick the framework your app uses: Import the SDK: ```swift highlight-integrateSwiftUI-import import IMGLYEngine import SwiftUI ``` `Canvas(engine:)` adopts the engine's Metal view into your SwiftUI hierarchy. The `.metal` render context is the default, so `Engine(license:userID:)` creates its own view. Start the engine in `onAppear`, hold it in `@State`, and seed a scene so the canvas shows content on launch: ```swift highlight-integrateSwiftUI-canvas struct IntegrateWithSwiftUI: View { @State private var engine: Engine? var body: some View { Group { if let engine { Canvas(engine: engine) } else { ProgressView("Starting the engine…") } } .onAppear { guard engine == nil else { return } Task { do { let engine = try await Engine( license: secrets.licenseKey, // pass nil for evaluation mode with watermark userID: "", ) 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 text = try engine.block.create(.text) try engine.block.setString(text, property: "text/text", value: "Hello, CE.SDK!") try engine.block.setPositionX(text, value: 80) try engine.block.setPositionY(text, value: 260) try engine.block.setWidth(text, value: 640) try engine.block.appendChild(to: page, child: text) try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) self.engine = engine } catch { print("Engine setup failed: \(error)") } } } } } ``` Present `IntegrateWithSwiftUI` as your app's root view: in the `App` file Xcode generated, replace `ContentView()` inside the `WindowGroup` with `IntegrateWithSwiftUI()`. Import the SDK: ```swift highlight-integrateUIKit-import import IMGLYEngine import MetalKit import UIKit ``` For a UIKit app, own an `MTKView` and hand it to the engine with the `.metalView(view:)` context. Forward `viewDidAppear` / `viewWillDisappear` to `engine.onAppear()` / `engine.onDisappear()` — forward-compatible lifecycle hooks that are no-ops in the engine today but part of the public API: ```swift highlight-integrateUIKit-canvas final class IntegrateWithUIKit: UIViewController { private var engine: Engine? private lazy var canvas = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice()) private lazy var spinner: UIActivityIndicatorView = { let indicator = UIActivityIndicatorView(style: .large) indicator.translatesAutoresizingMaskIntoConstraints = false indicator.hidesWhenStopped = true return indicator }() override func viewDidLoad() { super.viewDidLoad() view.addSubview(canvas) canvas.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ canvas.leftAnchor.constraint(equalTo: view.leftAnchor), canvas.rightAnchor.constraint(equalTo: view.rightAnchor), canvas.topAnchor.constraint(equalTo: view.topAnchor), canvas.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) view.addSubview(spinner) NSLayoutConstraint.activate([ spinner.centerXAnchor.constraint(equalTo: view.centerXAnchor), spinner.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) spinner.startAnimating() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) guard engine == nil else { return } Task { do { let engine = try await Engine( context: .metalView(view: canvas), license: secrets.licenseKey, // pass nil for evaluation mode with watermark userID: "", ) engine.onAppear() 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 text = try engine.block.create(.text) try engine.block.setString(text, property: "text/text", value: "Hello, CE.SDK!") try engine.block.setPositionX(text, value: 80) try engine.block.setPositionY(text, value: 260) try engine.block.setWidth(text, value: 640) try engine.block.appendChild(to: page, child: text) try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) self.engine = engine spinner.stopAnimating() } catch { print("Engine setup failed: \(error)") } } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) engine?.onDisappear() } } ``` Make `IntegrateWithUIKit` your initial view controller: open `Main.storyboard`, select the View Controller, and set its Custom Class to `IntegrateWithUIKit` in the Identity inspector. Alternatively, move the code above into the generated `ViewController` class. The example reads the license from a small `secrets` helper the guides repository ships ([source](https://github.com/imgly/cesdk-swift-examples/blob/v$UBQ_VERSION$/secrets/Secrets.swift)); replace `secrets.licenseKey` with your own CE.SDK license key string, or pass `nil` for evaluation mode with a watermark. Because `Engine` is `@MainActor`-isolated, the compiler enforces that every engine call runs on the main thread. Select the **My Mac (Mac Catalyst)** run destination, then Build and Run. The engine renders your page with the "Hello, CE.SDK!" text on the canvas. ## Using Your App The canvas displays the scene, but Mac Catalyst has no built-in toolbar or panels — that part is yours to build. Pair the canvas with your own controls, add and configure blocks through the same `engine.block` and `engine.scene` APIs, and export the result with `engine.block.export(_:mimeType:)`. The [Build Your Own UI](https://img.ly/docs/cesdk/mac-catalyst/user-interface/build-your-own-ui-fe7527/) guide walks through wiring a toolbar, a property inspector, and export into a complete custom editor. ## Troubleshooting If you run into issues, here are some common problems and solutions. For additional help, [visit our support page](https://img.ly/company/contact-us). #### Package Won't Add to Your Mac Catalyst Target Make sure you added the `IMGLYEngine-swift` package. The `IMGLYUI-swift` package (the prebuilt editor and camera) builds on iOS only and cannot link against a Mac Catalyst target. #### Import Errors: 'Engine' or 'Canvas' Not Found Every Swift file that uses the engine needs `import IMGLYEngine` before the first line of code. Confirm the `IMGLYEngine` library is listed under `Frameworks, Libraries, and Embedded Content` on your target's `General` tab. #### License Key Error at Runtime Double-check that the license value passed to `Engine(license:userID:)` is the exact key with proper capitalization. If you don't have a license, [register for a free trial](https://img.ly/forms/free-trial) to get a demonstration license. Pass `nil` to run in evaluation mode with a watermark. #### Canvas Is Blank `Canvas(engine:)` and the `.metalView(view:)` context both require an engine created with a Metal context. Confirm `engine.scene.create()` ran and that you appended a page to the scene — an empty scene has nothing to render. ## Next Steps - [Build Your Own UI](https://img.ly/docs/cesdk/mac-catalyst/user-interface/build-your-own-ui-fe7527/) — Wire the engine to your own SwiftUI or UIKit controls to build a complete custom editor. - [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) — Explore the engine's six API namespaces for scenes, blocks, assets, and more. - [What is CE.SDK?](https://img.ly/docs/cesdk/mac-catalyst/what-is-cesdk-2e7acd/) — Understand the SDK's architecture and where the engine fits. --- ## 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: "MCP Server" description: "Connect AI assistants to CE.SDK documentation using the Model Context Protocol (MCP) server." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/get-started/mcp-server-fde71c/" --- > 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:** [Build with AI](https://img.ly/docs/cesdk/mac-catalyst/get-started/build-with-ai-k7m9p2/) > [MCP Server](https://img.ly/docs/cesdk/mac-catalyst/get-started/mcp-server-fde71c/) --- The CE.SDK MCP server provides a standardized interface that allows any compatible AI assistant to search and access our documentation. This enables AI tools like Claude, Cursor, and VS Code Copilot to provide more accurate, context-aware help when working with CE.SDK. ## What is MCP? The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that enables AI assistants to securely connect to external data sources. By connecting your AI tools to our MCP server, you get: - **Accurate answers**: AI assistants can search and retrieve the latest CE.SDK documentation - **Context-aware help**: Get platform-specific guidance for your development environment - **Up-to-date information**: Always access current documentation without relying on training data ## Available Tools The MCP server exposes two tools: | Tool | Description | | -------- | --------------------------------------------- | | `search` | Search documentation by query string | | `fetch` | Retrieve the full content of a document by ID | ## Server Endpoint | URL | Transport | | ------------------------ | --------------- | | `https://mcp.img.ly/mcp` | Streamable HTTP | No authentication is required. ## Setup Instructions ### Claude Code Add the MCP server with a single command: ```bash claude mcp add --transport http imgly_docs https://mcp.img.ly/mcp ``` ### Claude Desktop 1. Open Claude Desktop and go to **Settings** (click your profile icon) 2. Navigate to **Connectors** in the sidebar 3. Click **Add custom connector** 4. Enter the URL: `https://mcp.img.ly/mcp` 5. Click **Add** to connect ### Cursor Add the following to your Cursor MCP configuration. You can use either: - **Project-specific**: `.cursor/mcp.json` in your project root - **Global**: `~/.cursor/mcp.json` ```json { "mcpServers": { "imgly_docs": { "url": "https://mcp.img.ly/mcp" } } } ``` ### VS Code Add to your workspace configuration at `.vscode/mcp.json`: ```json { "servers": { "imgly_docs": { "type": "http", "url": "https://mcp.img.ly/mcp" } } } ``` ### Windsurf Add the following to your Windsurf MCP configuration at `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "imgly_docs": { "serverUrl": "https://mcp.img.ly/mcp" } } } ``` ### Other Clients For other MCP-compatible clients, use the endpoint `https://mcp.img.ly/mcp` with HTTP transport. Refer to your client's documentation for the specific configuration format. ## Usage Once configured, your AI assistant will automatically have access to CE.SDK documentation. You can ask questions like: - "How do I add a text block in CE.SDK?" - "Show me how to export a design as PNG" - "What are the available blend modes?" The AI will search our documentation and provide answers based on the latest CE.SDK guides and API references. --- ## 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: "Get Started" description: "Start integrating CE.SDK into your application—from understanding the SDK to running your first editor." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/" --- > 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/) --- Everything you need to integrate CE.SDK into your application. Learn what the SDK offers, get up and running with starter kits, explore AI-powered workflows, and understand our licensing model. --- ## Related Pages - [Mac Catalyst Creative Editor](https://img.ly/docs/cesdk/mac-catalyst/what-is-cesdk-2e7acd/) - Learn what CE.SDK is, how it works, and what you can build with its UI, headless API, and real-time design engine. - [Capabilities](https://img.ly/docs/cesdk/mac-catalyst/capabilities-e1906f/) - Explore the full list of CE.SDK capabilities available for your platform, including design, video, image, text, and more. - [Quickstart](https://img.ly/docs/cesdk/mac-catalyst/get-started/mac-catalyst/quickstart-mcat0q/) - Integrate the CE.SDK Engine into a Mac Catalyst app and host the canvas with SwiftUI or UIKit. - [Licensing](https://img.ly/docs/cesdk/mac-catalyst/licensing-8aa063/) - Understand CE.SDK’s flexible licensing, trial options, and how keys work across dev, staging, and production. --- ## 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: "Guides" description: "Documentation for Guides" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/guides-8d8b00/" --- > 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/) --- --- ## Related Pages - [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) - Explore all configurable editor settings and learn how to read, update, and observe them via the Settings API. - [Serve Assets From Your Server](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) - Set up and manage how assets are served to the editor, including local, remote, or CDN-based delivery. - [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) - Understand CE.SDK's architecture and learn when to use direct Engine access for automation workflows - [Automate Workflows](https://img.ly/docs/cesdk/mac-catalyst/automation-715209/) - Automate repetitive editing tasks using CE.SDK’s headless APIs to generate assets at scale. - [User Interface](https://img.ly/docs/cesdk/mac-catalyst/user-interface-5a089a/) - Use CE.SDK’s customizable, production-ready UI or replace it entirely with your own interface. - [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) - Learn how to load and create scenes, set the zoom level, and configure URI resolvers. - [Insert Media Into Scenes](https://img.ly/docs/cesdk/mac-catalyst/insert-media-a217f5/) - Understand how insertion works, how inserted media behave within scenes, and how to control them via UI or code. - [Import Media](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) - Learn how to import, manage, and customize assets from local, remote, or camera sources in CE.SDK. - [Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) - Explore export options, supported formats, and configuration features for sharing or rendering output. - [Save](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) - Save design progress locally or to a backend service to allow for later editing or publishing. - [Store Custom Metadata](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/store-custom-metadata-337248/) - Attach, retrieve, and manage custom key-value metadata on design blocks in CE.SDK. - [Edit Image](https://img.ly/docs/cesdk/mac-catalyst/edit-image-c64912/) - Use CE.SDK to crop, transform, annotate, or enhance images with editing tools and programmatic APIs. - [Create Videos](https://img.ly/docs/cesdk/mac-catalyst/create-video-c41a08/) - Learn how to create and customize videos in CE.SDK using scenes, assets, and time-based editing. - [Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio-2f700b/) - Create audio blocks, extract tracks from video, control playback, generate waveforms, and manage audio timing in CE.SDK Engine for Swift. - [Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) - Add, style, and customize text layers in your design using CE.SDK’s flexible text editing tools. - [Create and Edit Shapes](https://img.ly/docs/cesdk/mac-catalyst/shapes-9f1b2c/) - Draw custom vector shapes, combine them with boolean operations, and insert QR codes into your designs. - [Create and Edit Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-3d4e5f/) - Create and customize stickers using image fills for icons, logos, emoji, and multi-color graphics. - [Create Compositions](https://img.ly/docs/cesdk/mac-catalyst/create-composition-db709c/) - Combine and arrange multiple elements to create complex, multi-page, or layered design compositions. - [Create Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) - Learn how to create, import, and manage reusable templates to streamline design creation in CE.SDK. - [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) - Manage color usage in your designs, from applying brand palettes to handling print and screen formats. - [Fills](https://img.ly/docs/cesdk/mac-catalyst/fills-402ddc/) - Apply solid colors, gradients, images, or videos as fills to shapes, text, and other design elements. - [Outlines](https://img.ly/docs/cesdk/mac-catalyst/outlines-b7820c/) - Enhance design elements with strokes, shadows, and glow effects to improve contrast and visual appeal. - [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) - Enhance visual elements with filters and effects such as blur, duotone, LUTs, and chroma keying. - [Animation](https://img.ly/docs/cesdk/mac-catalyst/animation-ce900c/) - Add motion to designs with entrance, exit, and loop animation presets, timing controls, and programmatic APIs. - [Rules](https://img.ly/docs/cesdk/mac-catalyst/rules-1427c0/) - Define and enforce layout, branding, and safety rules to ensure consistent and compliant designs. - [Conversion](https://img.ly/docs/cesdk/mac-catalyst/conversion-c3fbb3/) - Convert designs into different formats such as PDF, PNG, MP4, and more using CE.SDK tools. - [Improve Performance](https://img.ly/docs/cesdk/mac-catalyst/performance-3c12eb/) - Optimize CE.SDK integration on Apple platforms with source sets, memory monitoring, export tuning, and lifecycle best practices. - [Create a precompiled XCFramework for offline builds](https://img.ly/docs/cesdk/mac-catalyst/create-prebuilt-xcframework-c67971/) - Compiling CE.SDK Swift packages and other project dependencies to a binary XCFramework to support easy building in airgapped environments. --- ## 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 Media" description: "Learn how to import, manage, and customize assets from local, remote, or camera sources in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/import-media/overview-84bb23/) - Learn how to import, manage, and customize assets from local, remote, or camera sources in CE.SDK. - [Asset Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) - This guide explains the foundational architecture of the CE.SDK asset system, including what asset sources are, how they organize content, and how they connect to the user interface. - [Asset Library](https://img.ly/docs/cesdk/mac-catalyst/import-media/asset-library-65d6c4/) - Manage how users browse, preview, and insert media assets into their designs with a customizable asset library. - [Import From Local Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-local-source-39b2a9/) - Enable users to upload files from their device for use as design assets in the editor. - [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) - Connect CE.SDK to external sources like servers or third-party platforms to import assets remotely. - [Edit or Remove Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/edit-or-remove-assets-ce072c/) - Manage assets in local asset sources by updating metadata, removing individual assets, or deleting entire sources in CE.SDK. - [Source Sets](https://img.ly/docs/cesdk/mac-catalyst/import-media/source-sets-5679c8/) - Provide multiple versions of images and videos at different resolutions for optimal performance and quality across editing and export workflows. - [Using Default Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/default-assets-d2763d/) - Load shapes, stickers, images, and other built-in assets from IMG.LY's CDN to populate your CE.SDK editor using the Asset API. - [Retrieve MIME Type](https://img.ly/docs/cesdk/mac-catalyst/import-media/retrieve-mimetype-ed13bf/) - Detect the MIME type of resources loaded in the engine to determine file formats for processing, export, or display. - [Create a Custom Importer](https://img.ly/docs/cesdk/mac-catalyst/import-media/create-custom-importer-0f7e16/) - Import an unsupported file format into CE.SDK by parsing it yourself and rebuilding it as editable blocks with the Swift Scene and Block APIs. - [Asset Content JSON Schema](https://img.ly/docs/cesdk/mac-catalyst/import-media/content-json-schema-a7b3d2/) - Understand the JSON schema structure for defining asset source content including version, metadata, and payload properties for images, videos, fonts, and templates. - [Supported File Formats for Import](https://img.ly/docs/cesdk/mac-catalyst/import-media/file-format-support-8cdc84/) - Review the supported image, video, audio, and template formats for importing assets into CE.SDK. - [Size Limits](https://img.ly/docs/cesdk/mac-catalyst/import-media/size-limits-c32275/) - Learn about file size restrictions and how to optimize large assets for use 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: "Asset Library" description: "Manage how users browse, preview, and insert media assets into their designs with a customizable asset library." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/asset-library-65d6c4/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Asset Library](https://img.ly/docs/cesdk/mac-catalyst/import-media/asset-library-65d6c4/) --- --- ## Related Pages - [Thumbnails](https://img.ly/docs/cesdk/mac-catalyst/import-media/asset-library/thumbnails-c23949/) - Configure thumbnail images for assets in CE.SDK's asset library with proper sizing, preview URIs for audio, and custom asset sources. --- ## 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: "Thumbnails" description: "Configure thumbnail images for assets in CE.SDK's asset library with proper sizing, preview URIs for audio, and custom asset sources." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/asset-library/thumbnails-c23949/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Asset Library](https://img.ly/docs/cesdk/mac-catalyst/import-media/asset-library-65d6c4/) > [Thumbnails](https://img.ly/docs/cesdk/mac-catalyst/import-media/asset-library/thumbnails-c23949/) --- ```swift file=@cesdk_swift_examples/engine-guides-thumbnails/Thumbnails.swift reference-only import Foundation import IMGLYEngine // MARK: - Custom Asset Source // A custom asset source backed by an external photo service. private final class StockPhotoSource: NSObject, AssetSource { let id = "stock-photos" var supportedMIMETypes: [String]? { ["image/jpeg"] } var credits: AssetCredits? { nil } var license: AssetLicense? { nil } private let host = "https://cdn.img.ly/packages/imgly/cesdk-swift/1.80.0/assets" func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { let asset = AssetResult( id: "mountain-lake", label: "Mountain Lake", meta: [ "uri": "\(host)/ly.img.image/images/sample_1.jpg", "thumbUri": "\(host)/ly.img.image/thumbnails/sample_1.jpg", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", ], context: AssetContext(sourceID: id), ) return AssetQueryResult( assets: [asset], currentPage: queryData.page, nextPage: -1, total: 1, ) } } // MARK: - Guide @MainActor func thumbnails(engine: Engine) async throws { // Base path the example asset URIs are built from. Replace with your own host. let baseURL = "https://cdn.img.ly/packages/imgly/cesdk-swift/1.80.0/assets" try engine.asset.addLocalSource(sourceID: "my-images") let image = AssetDefinition( id: "scenic-landscape", meta: [ "uri": "\(baseURL)/ly.img.image/images/sample_1.jpg", "thumbUri": "\(baseURL)/ly.img.image/thumbnails/sample_1.jpg", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Scenic Landscape"], ) try engine.asset.addAsset(to: "my-images", asset: image) try engine.asset.addLocalSource(sourceID: "my-audio", supportedMimeTypes: ["audio/x-m4a"]) let audio = AssetDefinition( id: "ambient-track", meta: [ "uri": "\(baseURL)/ly.img.audio/audios/dance_harder.m4a", "thumbUri": "\(baseURL)/ly.img.audio/thumbnails/dance_harder.jpg", "previewUri": "\(baseURL)/ly.img.audio/audios/dance_harder.m4a", "mimeType": "audio/x-m4a", ], label: ["en": "Ambient Track"], ) try engine.asset.addAsset(to: "my-audio", asset: audio) let stockSource = StockPhotoSource() try engine.asset.addSource(stockSource) // Confirm each source returns its assets with the configured metadata. for sourceID in ["my-images", "my-audio", "stock-photos"] { let results = try await engine.asset.findAssets( sourceID: sourceID, query: .init(query: nil, page: 0, perPage: 10), ) print("\(sourceID):", results.total, "asset(s)") } } ``` Thumbnails give assets a visual preview in the asset library, helping users browse images, audio, and other media. This guide configures thumbnail and preview metadata for local and custom asset sources with the Swift Engine API. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-thumbnails) Thumbnails are configured through an asset's metadata. When you register an asset, you provide a `thumbUri` that the asset library displays as the asset's preview, separate from the full-resolution `uri` used on the canvas. We recommend a **512px width** for `thumbUri` to keep previews crisp without loading the full asset. The snippets below build their asset URLs from a `baseURL` constant pointing at the CE.SDK asset CDN — replace it with your own asset host. ## Understanding thumbUri vs previewUri Three URI properties control how an asset is displayed and used: | Property | Purpose | Used For | Media Type | Set On Block | |----------|---------|----------|------------|--------------| | `thumbUri` | Visual thumbnail (UI-only) | Asset library grid display | **Image only** | No | | `previewUri` | Preview content | Audio playback in the library, and set as a block property on the canvas | **Any media type** | Yes | | `uri` | Full asset | Final content on the canvas | Any | Yes | The `thumbUri` is UI-only and must be a raster image. It appears in the asset library but is never set on the block itself. The `previewUri` is set as a property on the block when the asset is applied to the canvas. It can be any media type and serves both as the library playback preview and as the block's preview content. For images, only `thumbUri` and `uri` are needed. For audio, all three are useful: `thumbUri` shows a waveform image in the library, `previewUri` provides a short clip for playback, and `uri` loads the full file for export. The `previewUri` is a performance optimization for large audio files — without it the engine loads the full `uri` for preview playback, which is slow for multi-minute tracks. ## Thumbnail Configuration ### Basic Thumbnails Register a local source with `addLocalSource(sourceID:)`, then add an asset whose metadata includes a `thumbUri` alongside the full-resolution `uri`. ```swift highlight-thumbnails-basic try engine.asset.addLocalSource(sourceID: "my-images") let image = AssetDefinition( id: "scenic-landscape", meta: [ "uri": "\(baseURL)/ly.img.image/images/sample_1.jpg", "thumbUri": "\(baseURL)/ly.img.image/thumbnails/sample_1.jpg", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Scenic Landscape"], ) try engine.asset.addAsset(to: "my-images", asset: image) ``` The `thumbUri` points to a 512px-wide image. The asset library renders this thumbnail in its grid while the canvas uses the full-resolution `uri`. ### Preview URIs for Audio Audio assets add a `previewUri` and a `mimeType`. The `previewUri` serves two purposes: in an audio section of the asset library, the play button streams it (falling back to `uri`) instead of the full file, and when the asset is added to the canvas it is set as a block property for preview playback. ```swift highlight-thumbnails-audioPreview try engine.asset.addLocalSource(sourceID: "my-audio", supportedMimeTypes: ["audio/x-m4a"]) let audio = AssetDefinition( id: "ambient-track", meta: [ "uri": "\(baseURL)/ly.img.audio/audios/dance_harder.m4a", "thumbUri": "\(baseURL)/ly.img.audio/thumbnails/dance_harder.jpg", "previewUri": "\(baseURL)/ly.img.audio/audios/dance_harder.m4a", "mimeType": "audio/x-m4a", ], label: ["en": "Ambient Track"], ) try engine.asset.addAsset(to: "my-audio", asset: audio) ``` The `thumbUri` supplies the waveform image shown beside the track, and the `mimeType` declares the asset's format — pair it with the source's `supportedMimeTypes` to control which audio files the source accepts. In production, point `previewUri` at a shorter clip (around 30 seconds) of the same track; this example reuses the full track URL for brevity. ### Custom Asset Source Thumbnails A custom source that fetches from an external service maps that service's response onto CE.SDK's metadata inside `findAssets(queryData:)`. Map the high-resolution image to `uri` and the service's small image to `thumbUri`. ```swift highlight-thumbnails-customSource // A custom asset source backed by an external photo service. private final class StockPhotoSource: NSObject, AssetSource { let id = "stock-photos" var supportedMIMETypes: [String]? { ["image/jpeg"] } var credits: AssetCredits? { nil } var license: AssetLicense? { nil } private let host = "https://cdn.img.ly/packages/imgly/cesdk-swift/1.80.0/assets" func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { let asset = AssetResult( id: "mountain-lake", label: "Mountain Lake", meta: [ "uri": "\(host)/ly.img.image/images/sample_1.jpg", "thumbUri": "\(host)/ly.img.image/thumbnails/sample_1.jpg", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", ], context: AssetContext(sourceID: id), ) return AssetQueryResult( assets: [asset], currentPage: queryData.page, nextPage: -1, total: 1, ) } } ``` Register the source with `addSource(_:)` so its assets — thumbnails included — appear in the asset library. ```swift highlight-thumbnails-registerCustomSource let stockSource = StockPhotoSource() try engine.asset.addSource(stockSource) ``` ## Display Customization On iOS, the editor's asset library shows each asset's `thumbUri` (falling back to `uri`) using a built-in preview that depends on the asset type: images and videos fill a square cell, shapes and stickers fit within one, and audio appears as a list row beside its waveform. This rendering is fixed — it is not driven by asset metadata or exposed as a public setting. What you can customize is the asset library's structure: which sources appear, how they are grouped into sections and tabs, and which built-in section style each uses. See the [Customize Asset Library](#broken-link-c9a4de) guide. ## Best Practices - **Size**: Use a 512px width for `thumbUri` to keep previews sharp across devices. - **Format**: `thumbUri` must be a raster image — use JPEG for photos and PNG for graphics with transparency. SVG thumbnails do not render in the native asset library. - **When to use previewUri**: - Audio: Provide a shorter preview clip (around 30 seconds instead of several minutes). - Video: Not supported — use `thumbUri` and `uri` only. - Images: Not needed — `thumbUri` is sufficient. - **Media type constraints**: `thumbUri` must be an image, while `previewUri` can be any media type (currently used for audio). - **Block property**: Unlike `thumbUri` (UI-only), `previewUri` is set as a property on the block when the asset is applied to the canvas. - **Performance**: Optimize thumbnail file sizes and serve them from a CDN with cache headers. ## Troubleshooting **Thumbnails not displaying**: Verify the `thumbUri` URL resolves and points to a raster image (PNG or JPEG). SVG thumbnails do not render in the native asset library. **Audio preview not working**: Confirm `previewUri` (or `uri` as a fallback) points to a valid, reachable audio file — that is what the play button streams. Include a `mimeType` so the source accepts the asset and the engine treats it as audio. **Slow loading**: Keep thumbnails small (around 512px wide) and, for audio, point `previewUri` at a short clip rather than the full track. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addLocalSource(sourceID:supportedMimeTypes:)` | Register a local source that assets can be added to | | `engine.asset.addAsset(to:asset:)` | Add an `AssetDefinition` with thumbnail metadata to a source | | `engine.asset.addSource(_:)` | Register a custom `AssetSource` | | `engine.asset.findAssets(sourceID:query:)` | Query a source's assets | ### Properties | Property | Type | Description | | --- | --- | --- | | `uri` | String | Full asset used on the canvas | | `thumbUri` | String | Raster thumbnail shown in the asset library grid (512px recommended) | | `previewUri` | String | Preview content set on the block; used for audio playback in the library | | `mimeType` | String | MIME type of the asset; required for audio preview buttons | ## Next Steps - [Customize Asset Library](#broken-link-c9a4de) — On iOS, customize the asset library UI - [Asset Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) — Asset sources and metadata - [Unsplash Integration](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/) — Thumbnail mapping example - [Source Sets](https://img.ly/docs/cesdk/mac-catalyst/import-media/source-sets-5679c8/) — Responsive asset rendering (not thumbnails) --- ## 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: "Asset Concepts" description: "This guide explains the foundational architecture of the CE.SDK asset system, including what asset sources are, how they organize content, and how they connect to the user interface." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) --- Understand the foundational architecture of CE.SDK's asset system and how asset sources organize content across platforms. Asset sources are CE.SDK's content delivery architecture. Instead of hardcoding asset knowledge into the engine, CE.SDK uses a modular system where any content can be provided through a standardized interface. This decouples what assets are available from how they're discovered and applied. ``` PLATFORM-SPECIFIC UI (iOS editor) ┌─────────────────────────────────────────────────────────────────────────┐ │ ┌─────────────┐ ┌───────────────┐ ┌─────────────┐ │ │ │ Dock Button │───▶│ Asset Library │───▶│ Assets Grid │ │ │ └─────────────┘ └───────────────┘ └─────────────┘ │ │ │ │ Configured via: Asset Library config, Dock config │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ CROSS-PLATFORM ENGINE (engine.asset API) ┌─────────────────────────────────────────────────────────────────────────┐ │ │ │ findAssets() addSource() addLocalSource() apply() │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Custom │ │ Local │ │ JSON-Based │ │ │ │ Sources │ │ Sources │ │ Sources │ │ │ ├──────────────┤ ├──────────────┤ ├──────────────┤ │ │ │ Your API │ │ User Uploads │ │ Built-in │ │ │ │ Database │ │ Collections │ │ Asset Packs │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ Identical across every platform CE.SDK supports │ └─────────────────────────────────────────────────────────────────────────┘ ``` This guide covers the foundational concepts of asset sources. For implementation details, see the linked guides at the end. ## Asset Source Fundamentals An asset source provides content to the engine through a common interface. Every source has a unique identifier (e.g., `ly.img.image`, `ly.img.sticker`) and implements methods for discovering and applying assets. Sources support: - **Query-based discovery** with pagination and filtering - **Optional grouping** (e.g., sticker groups: "emoji", "doodle", "hand") - **Metadata** including credits, licenses, and format information Sources are content-agnostic—images, fonts, templates, and custom content all use the same pattern. ## Content Organized as Asset Sources Asset sources handle virtually all reusable creative content: | Category | Examples | | ---------- | ---------------------------------------- | | Media | Images, videos, audio clips | | Graphics | Stickers, shapes, vectors, icons | | Typography | Fonts, typefaces, text presets | | Colors | Color palettes, spot colors | | Effects | Blur types, filters, LUT effects | | Templates | Design templates, page presets | | Custom | User uploads, remote APIs, your own data | Built-in sources include `ly.img.image`, `ly.img.sticker`, `ly.img.templates`, `ly.img.typeface`, `ly.img.filter.lut`, `ly.img.blur`, `ly.img.effect`, and more. ## Types of Asset Sources There are three ways to provide assets to CE.SDK: ### Custom Sources Conform to the `AssetSource` protocol to connect any backend—database, API, or custom system. Custom sources provide full control over discovery and application logic. Use a custom source when you need to: - Connect to your existing content management system - Implement custom search or filtering logic - Control how assets are applied to the scene ### Local Sources Managed by the engine with dynamic add and remove operations. Local sources are suitable for user uploads or custom collections that change during the editing session. The engine handles storage and retrieval. ### JSON-Based Sources Pre-defined asset collections loaded from JSON files. All built-in asset packs use this approach. JSON sources are ideal for static content that doesn't change frequently. ## Asset Sources and the User Interface Asset sources are backend providers—they don't know about UI. The connection between sources and what users see happens through editor configuration. On iOS, the editor's asset library presents sources to users. You configure which sources appear and how they're grouped through the asset library configuration, and the same sources surface from dock buttons that open the library. This separation means you can: - Show multiple sources in one library section - Show the same source in different locations - Change the presentation without changing the source macOS and Mac Catalyst apps that drive the engine directly work with the `engine.asset` API and present results in whatever interface they build. ## Cross-Platform Architecture Asset sources use the `engine.asset` API consistently across every platform CE.SDK supports. All platforms support: - Custom source registration - JSON-based asset loading - Local asset management - Group-based organization - Event subscriptions (source added, removed, updated) Code patterns transfer directly between platforms with only syntax changes. ## Asset Structure Each asset contains: - **ID** — Unique identifier within the source - **Meta** — URI, thumbnail, MIME type, dimensions, block type hints - **Label** — Localized display name - **Tags** — Searchable keywords (localized) - **Groups** — Category membership - **Context** — Source reference for tracking origin The engine uses metadata hints (`blockType`, `fillType`, `shapeType`) to determine what block type to create when applying an asset. ## Discovery and Application Assets are discovered through queries supporting pagination, text search, tag and group filtering, and sorting. When applied, assets either create new blocks or modify existing ones. Sources can customize application behavior or use the engine's default implementation. ## Source Lifecycle Events The engine emits events when sources change: added, removed, or contents updated. Subscribe to these events to keep available content synchronized. ## Troubleshooting Common conceptual misunderstandings: - **Confusing sources with UI** — Asset sources are backend providers; they don't render UI. The asset library presents them, and you configure that presentation separately. - **Expecting sources to filter themselves** — Sources return all matching assets; the configuration determines what's displayed to users. - **Mixing source types** — Custom sources (your code), local sources (engine-managed), and JSON sources (static files) serve different purposes. Choose based on whether you need dynamic backend connections, runtime asset management, or static asset packs. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addSource(_:)` | Register a custom asset source with discovery and apply callbacks | | `engine.asset.addLocalSource(sourceID:)` | Create an engine-managed source for dynamic asset add and remove | | `engine.asset.findAssets(sourceID:query:)` | Query assets with pagination, search, filtering, and sorting | | `engine.asset.apply(sourceID:assetResult:)` | Apply an asset to the active scene, creating a configured block | ### Events | Property | Type | Description | | --- | --- | --- | | `engine.asset.onAssetSourceAdded` | `AsyncStream` | Emits the ID of each newly registered source | | `engine.asset.onAssetSourceRemoved` | `AsyncStream` | Emits the ID of each removed source | | `engine.asset.onAssetSourceUpdated` | `AsyncStream` | Emits the ID of each source whose contents changed | ## Next Steps - [Your Server](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/your-server-b91910/) — Connect your own backend as an asset source - [Integrate Unsplash Stock Images](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/) — Follow an end-to-end remote custom asset source example - [Customize Asset Library](#broken-link-c9a4de) — On iOS, customize the asset library 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: "Asset Content JSON Schema" description: "Understand the JSON schema structure for defining asset source content including version, metadata, and payload properties for images, videos, fonts, and templates." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/content-json-schema-a7b3d2/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Asset Content JSON Schema](https://img.ly/docs/cesdk/mac-catalyst/import-media/content-json-schema-a7b3d2/) --- Reference documentation for the JSON schema structure used to define asset source content in CE.SDK. Asset content JSON files define the structure and metadata for assets that CE.SDK loads into asset sources. This schema supports images, videos, audio, fonts, templates, colors, shapes, and effects. ## Manifest Structure Every `content.json` file requires three top-level fields: ```json { "version": "2.0.0", "id": "my.custom.source", "assets": [] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `version` | `string` | Yes | Schema version | | `id` | `string` | Yes | Unique identifier for the asset source | | `assets` | `AssetDefinition[]` | Yes | Array of asset definitions | ## Asset Definition Each asset in the `assets` array follows this structure: | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | `string` | Yes | Unique identifier within the source | | `label` | `Record` | No | Localized display names for UI and tooltips | | `tags` | `Record` | No | Localized keywords for search and filtering | | `groups` | `string[]` | No | Categories for grouping assets in the UI | | `meta` | `AssetMetaData` | No | Content-specific metadata | | `payload` | `AssetPayload` | No | Structured data for specialized assets | ### Localization Labels and tags use locale codes as keys (e.g., `"en"`, `"de"`, `"fr"`). CE.SDK selects the appropriate translation based on the user's locale. ```json { "id": "mountain-photo", "label": { "en": "Mountain Landscape", "de": "Berglandschaft" }, "tags": { "en": ["nature", "mountain"], "de": ["natur", "berg"] }, "groups": ["landscapes", "nature"] } ``` ## Asset Metadata The `meta` object contains content-specific information for loading and applying assets. ### Content Properties Define URIs and file information for loading the asset content. The `uri` property points to the main asset file, while `thumbUri` and `previewUri` provide optimized versions for UI display. ```json { "meta": { "uri": "{{base_url}}/images/photo.jpg", "thumbUri": "{{base_url}}/thumbnails/photo-thumb.jpg", "previewUri": "{{base_url}}/previews/photo-preview.jpg", "filename": "photo.jpg", "mimeType": "image/jpeg" } } ``` | Property | Type | Description | |----------|------|-------------| | `uri` | `string` | Primary content URI. Supports `{{base_url}}` placeholder | | `thumbUri` | `string` | Thumbnail image URI for previews | | `previewUri` | `string` | Higher-quality preview URI | | `filename` | `string` | Original filename | | `mimeType` | `string` | MIME type (e.g., `"image/jpeg"`, `"video/mp4"`) | ### Dimension Properties Specify the pixel dimensions of the asset. CE.SDK uses these values for layout calculations and aspect ratio preservation when inserting assets into a design. ```json { "meta": { "width": 1920, "height": 1280 } } ``` | Property | Type | Description | |----------|------|-------------| | `width` | `number` | Content width in pixels | | `height` | `number` | Content height in pixels | ### Block Creation Properties Control what design block CE.SDK creates when the asset is applied. These properties determine how the asset integrates into the design structure. ```json { "meta": { "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "shapeType": "//ly.img.ubq/shape/rect", "kind": "image" } } ``` | Property | Type | Description | |----------|------|-------------| | `blockType` | `string` | Design block type to create | | `fillType` | `string` | Fill type for the block | | `shapeType` | `string` | Shape type for stickers/shapes | | `kind` | `string` | Asset category hint (e.g., `"image"`, `"video"`, `"template"`) | **Block Type Values:** | Value | Use Case | |-------|----------| | `//ly.img.ubq/graphic` | Images, stickers, graphics | | `//ly.img.ubq/text` | Text blocks | | `//ly.img.ubq/audio` | Audio clips | | `//ly.img.ubq/page` | Templates, pages | | `//ly.img.ubq/group` | Grouped elements | | `//ly.img.ubq/cutout` | Cutout shapes | **Fill Type Values:** | Value | Use Case | |-------|----------| | `//ly.img.ubq/fill/image` | Image fills | | `//ly.img.ubq/fill/video` | Video fills | | `//ly.img.ubq/fill/color` | Solid color fills | | `//ly.img.ubq/fill/gradient/linear` | Linear gradients | | `//ly.img.ubq/fill/gradient/radial` | Radial gradients | | `//ly.img.ubq/fill/gradient/conical` | Conical gradients | **Shape Type Values:** | Value | Use Case | |-------|----------| | `//ly.img.ubq/shape/rect` | Rectangles | | `//ly.img.ubq/shape/ellipse` | Circles, ovals | | `//ly.img.ubq/shape/polygon` | Polygons | | `//ly.img.ubq/shape/star` | Star shapes | | `//ly.img.ubq/shape/line` | Lines | | `//ly.img.ubq/shape/vector_path` | Custom vector paths | ### Media Properties Configure playback behavior for time-based media like video and audio. Use `duration` to specify length and `looping` to enable repeat playback for background music or ambient video. ```json { "meta": { "duration": "30", "looping": true, "vectorPath": "M10 10 L90 90" } } ``` | Property | Type | Description | |----------|------|-------------| | `duration` | `string` | Duration in seconds as a string (e.g., `"30"`, `"120"`) | | `looping` | `boolean` | Whether media should loop continuously. Use for background music or ambient video | | `vectorPath` | `string` | SVG path data for vector shapes | ### Effect Properties Define visual effects that can be applied to design blocks. Effects include filters, blurs, and color adjustments. ```json { "meta": { "effectType": "//ly.img.ubq/effect/lut_filter", "blurType": "//ly.img.ubq/blur/uniform" } } ``` | Property | Type | Description | |----------|------|-------------| | `effectType` | `string` | Effect type (e.g., `"//ly.img.ubq/effect/lut_filter"`, `"//ly.img.ubq/effect/duotone_filter"`) | | `blurType` | `string` | Blur type: `"//ly.img.ubq/blur/uniform"`, `"//ly.img.ubq/blur/linear"`, `"//ly.img.ubq/blur/mirrored"`, `"//ly.img.ubq/blur/radial"` | ### Responsive Sources The `sourceSet` property defines multiple resolutions for responsive loading. This enables CE.SDK to load an appropriately sized image based on the display context, reducing bandwidth for thumbnails while providing full resolution when needed. ```json { "meta": { "sourceSet": [ { "uri": "{{base_url}}/small.jpg", "width": 640, "height": 480 }, { "uri": "{{base_url}}/medium.jpg", "width": 1280, "height": 960 }, { "uri": "{{base_url}}/large.jpg", "width": 1920, "height": 1440 } ] } } ``` When a user browses assets in the library panel, CE.SDK loads the smallest appropriate resolution. When the asset is added to the canvas and zoomed in, higher resolutions are loaded on demand. This pattern significantly improves initial load times for asset libraries with many items. | Property | Type | Required | Description | |----------|------|----------|-------------| | `uri` | `string` | Yes | Source URI | | `width` | `number` | Yes | Source width in pixels | | `height` | `number` | Yes | Source height in pixels | ## Asset Payload The `payload` object contains structured data for specialized asset types like colors, fonts, and presets. | Property | Type | Description | |----------|------|-------------| | `color` | `AssetColor` | Color definition | | `typeface` | `Typeface` | Font family definition | | `transformPreset` | `AssetTransformPreset` | Page size or aspect ratio preset | | `sourceSet` | `Source[]` | Responsive sources (same as meta.sourceSet) | ### Color Payload Colors support three color spaces: sRGB, CMYK, and Spot Color. Use sRGB for screen-based designs, CMYK for print workflows, and Spot Color for brand-specific colors that require exact color matching. **sRGB Color:** ```json { "payload": { "color": { "colorSpace": "sRGB", "r": 0.2, "g": 0.4, "b": 0.8 } } } ``` sRGB is the standard color space for web and digital displays. Component values range from 0 to 1, where `{ r: 1, g: 0, b: 0 }` represents pure red. | Property | Type | Range | Description | |----------|------|-------|-------------| | `colorSpace` | `"sRGB"` | — | Color space identifier | | `r` | `number` | 0–1 | Red component | | `g` | `number` | 0–1 | Green component | | `b` | `number` | 0–1 | Blue component | **CMYK Color:** ```json { "payload": { "color": { "colorSpace": "CMYK", "c": 0.75, "m": 0.25, "y": 0.0, "k": 0.1 } } } ``` CMYK is used for print production. Component values represent ink percentages from 0 to 1, where higher values mean more ink coverage. | Property | Type | Range | Description | |----------|------|-------|-------------| | `colorSpace` | `"CMYK"` | — | Color space identifier | | `c` | `number` | 0–1 | Cyan component | | `m` | `number` | 0–1 | Magenta component | | `y` | `number` | 0–1 | Yellow component | | `k` | `number` | 0–1 | Black (key) component | **Spot Color:** ```json { "payload": { "color": { "colorSpace": "SpotColor", "name": "Brand-Blue-286", "externalReference": "spot://brand-blue-286", "representation": { "colorSpace": "sRGB", "r": 0.0, "g": 0.22, "b": 0.62 } } } } ``` Spot colors reference named colors from a named-color system (for example, your in-house brand palette or a print vendor's spot-color library). The `representation` provides a screen preview while the actual color is defined by the external reference for accurate print reproduction. | Property | Type | Description | |----------|------|-------------| | `colorSpace` | `"SpotColor"` | Color space identifier | | `name` | `string` | Spot color name | | `externalReference` | `string` | External reference URI | | `representation` | `AssetRGBColor \| AssetCMYKColor` | Screen/print representation | ### Typeface Payload Defines a font family with multiple font files for different weights and styles. This enables CE.SDK to load the correct font file when text formatting changes. ```json { "payload": { "typeface": { "name": "Roboto", "fonts": [ { "uri": "{{base_url}}/Roboto-Regular.ttf", "subFamily": "Regular", "weight": "normal", "style": "normal" }, { "uri": "{{base_url}}/Roboto-Bold.ttf", "subFamily": "Bold", "weight": "bold", "style": "normal" }, { "uri": "{{base_url}}/Roboto-Italic.ttf", "subFamily": "Italic", "weight": "normal", "style": "italic" } ] } } } ``` Each font entry in the `fonts` array represents a single font file. When a user applies bold formatting, CE.SDK automatically selects the font entry with `weight: "bold"`. Include all weight and style combinations you want to support. **Typeface Properties:** | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | Yes | Typeface family name | | `fonts` | `Font[]` | Yes | Array of font definitions | **Font Properties:** | Property | Type | Required | Description | |----------|------|----------|-------------| | `uri` | `string` | Yes | Font file URI (.ttf, .otf, .woff, .woff2) | | `subFamily` | `string` | Yes | Font subfamily name (e.g., "Regular", "Bold Italic") | | `weight` | `FontWeight` | No | Font weight | | `style` | `FontStyle` | No | Font style | **Font Weight Values:** `"thin"`, `"extraLight"`, `"light"`, `"normal"`, `"medium"`, `"semiBold"`, `"bold"`, `"extraBold"`, `"heavy"` **Font Style Values:** `"normal"`, `"italic"` ### Transform Preset Payload Defines page size or aspect ratio presets for templates, canvases, and crop tools. Use these to provide users with common format options like social media dimensions or print sizes. **Fixed Size:** ```json { "payload": { "transformPreset": { "type": "FixedSize", "width": 1080, "height": 1920, "designUnit": "Pixel" } } } ``` Fixed size presets lock both width and height to specific values. Use `designUnit` to specify whether dimensions are in pixels (for digital), millimeters, or inches (for print). | Property | Type | Description | |----------|------|-------------| | `type` | `"FixedSize"` | Preset type | | `width` | `number` | Width value | | `height` | `number` | Height value | | `designUnit` | `string` | Unit: `"Pixel"`, `"Millimeter"`, or `"Inch"` | **Fixed Aspect Ratio:** ```json { "payload": { "transformPreset": { "type": "FixedAspectRatio", "width": 16, "height": 9 } } } ``` Fixed aspect ratio presets maintain proportions while allowing flexible sizing. The width and height values represent the ratio components, not pixel dimensions. | Property | Type | Description | |----------|------|-------------| | `type` | `"FixedAspectRatio"` | Preset type | | `width` | `number` | Aspect ratio width component | | `height` | `number` | Aspect ratio height component | **Free Aspect Ratio:** ```json { "payload": { "transformPreset": { "type": "FreeAspectRatio" } } } ``` Free aspect ratio presets allow unrestricted resizing without maintaining proportions. **Content Aspect Ratio:** ```json { "payload": { "transformPreset": { "type": "ContentAspectRatio" } } } ``` Content aspect ratio presets snap the block's frame to the intrinsic aspect ratio of its content, resolved from the fill's `sourceSet` when the preset is applied. Use this to revert a cropped image or video block to its natural proportions. Applying this preset to a block without resolvable content dimensions (e.g. a text block, empty placeholder, or page) returns an error. | Property | Type | Description | |----------|------|-------------| | `type` | `"ContentAspectRatio"` | Preset type | ## Base URL Placeholder The `{{base_url}}` placeholder enables portable asset definitions. CE.SDK replaces this placeholder with the actual base path when loading: - **From URL:** The parent directory of the JSON file becomes the base URL - **From string:** You provide the base URL explicitly when loading ```json { "meta": { "uri": "{{base_url}}/images/photo.jpg", "thumbUri": "{{base_url}}/thumbnails/photo.jpg" } } ``` ## Asset Type Examples ### Image Asset Standard image assets are the most common type, used for photos, illustrations, and background images. They require a `blockType` of graphic with an image fill. ```json { "id": "photo-001", "label": { "en": "Mountain Landscape" }, "tags": { "en": ["nature", "mountain"] }, "meta": { "uri": "{{base_url}}/mountain.jpg", "thumbUri": "{{base_url}}/mountain-thumb.jpg", "mimeType": "image/jpeg", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "width": 1920, "height": 1280 } } ``` ### Video Asset Video assets include duration information and use a video fill type. Set `looping` to `true` for videos that should repeat continuously. ```json { "id": "video-001", "label": { "en": "Intro Animation" }, "meta": { "uri": "{{base_url}}/intro.mp4", "thumbUri": "{{base_url}}/intro-thumb.jpg", "mimeType": "video/mp4", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/video", "width": 1920, "height": 1080, "duration": "5", "looping": false } } ``` ### Audio Asset Audio assets use the audio block type and don't require visual dimensions. Set `looping` to `true` for background music that should repeat continuously throughout the design. ```json { "id": "audio-001", "label": { "en": "Background Music" }, "meta": { "uri": "{{base_url}}/music.mp3", "mimeType": "audio/mpeg", "blockType": "//ly.img.ubq/audio", "duration": "120", "looping": true } } ``` ### Sticker Asset Stickers are vector graphics that maintain quality at any size. They use the `vector_path` shape type and typically reference SVG files. ```json { "id": "sticker-001", "label": { "en": "Star Badge" }, "meta": { "uri": "{{base_url}}/star.svg", "thumbUri": "{{base_url}}/star-thumb.png", "mimeType": "image/svg+xml", "blockType": "//ly.img.ubq/graphic", "shapeType": "//ly.img.ubq/shape/vector_path", "width": 200, "height": 200 } } ``` ### Template Asset Templates are complete design scenes that can be loaded as starting points. Use `kind: "template"` to identify them in the UI. ```json { "id": "template-001", "label": { "en": "Social Media Story" }, "meta": { "uri": "{{base_url}}/story-template.scene", "thumbUri": "{{base_url}}/story-thumb.jpg", "kind": "template", "width": 1080, "height": 1920 } } ``` ### Crop Preset Asset Crop presets define aspect ratios for the crop tool. Use `transformPreset` in the payload to specify the ratio without fixed pixel dimensions. ```json { "id": "crop-square", "label": { "en": "Square" }, "groups": ["social"], "payload": { "transformPreset": { "type": "FixedAspectRatio", "width": 1, "height": 1 } } } ``` ### Page Format Preset Asset Page format presets define canvas sizes for new designs. Use `FixedSize` to specify exact dimensions in pixels, millimeters, or inches. ```json { "id": "format-instagram-story", "label": { "en": "Instagram Story" }, "groups": ["social"], "meta": { "thumbUri": "{{base_url}}/instagram-story-thumb.jpg" }, "payload": { "transformPreset": { "type": "FixedSize", "width": 1080, "height": 1920, "designUnit": "Pixel" } } } ``` ## Troubleshooting | Issue | Solution | |-------|----------| | Assets not appearing | Verify `version`, `id`, and `assets` fields exist at the top level | | Invalid asset | Ensure each asset has a unique `id` | | Missing thumbnails | Check `thumbUri` points to accessible image URLs | | Base URL not resolving | Use exact `{{base_url}}` syntax (double curly braces) | | CORS errors | Configure server headers to allow cross-origin requests | | Wrong block created | Verify `meta.blockType` matches the intended design block | --- ## 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 Custom Importer" description: "Import an unsupported file format into CE.SDK by parsing it yourself and rebuilding it as editable blocks with the Swift Scene and Block APIs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/create-custom-importer-0f7e16/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Create a Custom Importer](https://img.ly/docs/cesdk/mac-catalyst/import-media/create-custom-importer-0f7e16/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-custom-importer/CreateCustomImporter.swift reference-only import Foundation import IMGLYEngine /// The intermediate model the importer decodes the source format into. In a real /// importer this mirrors the structure of your own file format. struct CustomImporterDesign: Decodable { let width: Float let height: Float let background: [Float]? // page background rgba in 0...1 let elements: [CustomImporterElement] } struct CustomImporterElement: Decodable { enum Kind: String, Decodable { case image case text case rectangle } let type: Kind let x: Float let y: Float let width: Float let height: Float let src: String? // image reference, for `.image` let text: String? // text content, for `.text` let color: [Float]? // rgba components in 0...1, for `.rectangle` } @MainActor func createCustomImporter(engine: Engine) async throws { // Resolve image references against the base URL where the importer's assets // live. Kept out of the highlighted snippets so the example runs offline. let baseURL = try engine.guidesBaseURL // The source bytes — here an inline string standing in for a file read from // disk, an upload, or your API. let sourceJSON = """ { "width": 800, "height": 600, "background": [1.0, 1.0, 1.0, 1.0], "elements": [ { "type": "rectangle", "x": 0, "y": 0, "width": 800, "height": 140, "color": [0.16, 0.20, 0.45, 1.0] }, { "type": "image", "x": 80, "y": 200, "width": 320, "height": 320, "src": "ly.img.image/images/sample_4.jpg" }, { "type": "text", "x": 440, "y": 250, "width": 300, "height": 120, "text": "Imported heading" } ] } """ let design = try JSONDecoder().decode(CustomImporterDesign.self, from: Data(sourceJSON.utf8)) let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: design.width) try engine.block.setHeight(page, value: design.height) if let background = design.background, background.count == 4 { let pageFill = try engine.block.createFill(.color) try engine.block.setColor( pageFill, property: "fill/color/value", color: .rgba(r: background[0], g: background[1], b: background[2], a: background[3]), ) try engine.block.setFill(page, fill: pageFill) } try engine.block.appendChild(to: scene, child: page) for element in design.elements { let block: DesignBlockID switch element.type { case .image: block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.image) if let src = element.src { try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent(src)) } try engine.block.setFill(block, fill: fill) case .rectangle: block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.color) if let color = element.color, color.count == 4 { try engine.block.setColor( fill, property: "fill/color/value", color: .rgba(r: color[0], g: color[1], b: color[2], a: color[3]), ) } try engine.block.setFill(block, fill: fill) case .text: block = try engine.block.create(.text) try engine.block.replaceText(block, text: element.text ?? "") try engine.block.setHeightMode(block, mode: .auto) } try engine.block.setPositionX(block, value: element.x) try engine.block.setPositionY(block, value: element.y) try engine.block.setWidth(block, value: element.width) // Text auto-sizes its height; every other element takes the source height. if element.type != .text { try engine.block.setHeight(block, value: element.height) } try engine.block.appendChild(to: page, child: block) } try await engine.captureGuide(page, label: "hero") try engine.scene.enableZoomAutoFit( page, axis: .both, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) let pages = try engine.scene.getPages() print("Imported design has \(pages.count) page(s)") } ``` Import a file format CE.SDK does not natively support by parsing it yourself and rebuilding its content as editable blocks with the Scene and Block APIs. ![A design reconstructed by a custom importer: a colored banner, an image, and a heading laid out on a page.](./assets/swift-based.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-create-custom-importer) CE.SDK natively opens its own scenes and archives, plus images and videos, and converts Photoshop and InDesign files through dedicated server-side packages. A custom importer covers everything else: when you have a proprietary or otherwise unsupported format, you parse it yourself and reconstruct its content as CE.SDK blocks. The engine never reads your file — it only sees the pages, graphics, and text blocks you create. ## How a Custom Importer Works An importer is a two-step pipeline: 1. **Parse** the source bytes into an intermediate model — your own types. 2. **Build** the scene by walking that model and calling the Block APIs. The engine has no knowledge of the source format; it only sees `engine.scene.create()` and the blocks you append afterward. Keeping parse and build separate lets you unit-test parsing without the engine and swap source formats without touching the build step. ## Define the Source Format Model the design as a page size plus a list of elements — each with a position, a size, and type-specific fields. This worked example uses a small JSON layout with image, text, and rectangle elements, decoded into `Decodable` structs. ```swift highlight-createCustomImporter-model /// The intermediate model the importer decodes the source format into. In a real /// importer this mirrors the structure of your own file format. struct CustomImporterDesign: Decodable { let width: Float let height: Float let background: [Float]? // page background rgba in 0...1 let elements: [CustomImporterElement] } struct CustomImporterElement: Decodable { enum Kind: String, Decodable { case image case text case rectangle } let type: Kind let x: Float let y: Float let width: Float let height: Float let src: String? // image reference, for `.image` let text: String? // text content, for `.text` let color: [Float]? // rgba components in 0...1, for `.rectangle` } ``` ## Parse the Source Decode the source bytes into the typed model with `JSONDecoder`. Here an inline string stands in for a file read from disk, an upload, or your API. ```swift highlight-createCustomImporter-parse // The source bytes — here an inline string standing in for a file read from // disk, an upload, or your API. let sourceJSON = """ { "width": 800, "height": 600, "background": [1.0, 1.0, 1.0, 1.0], "elements": [ { "type": "rectangle", "x": 0, "y": 0, "width": 800, "height": 140, "color": [0.16, 0.20, 0.45, 1.0] }, { "type": "image", "x": 80, "y": 200, "width": 320, "height": 320, "src": "ly.img.image/images/sample_4.jpg" }, { "type": "text", "x": 440, "y": 250, "width": 300, "height": 120, "text": "Imported heading" } ] } """ let design = try JSONDecoder().decode(CustomImporterDesign.self, from: Data(sourceJSON.utf8)) ``` ## Create the Scene and Page Create an empty design scene with `engine.scene.create()`, then add a page sized from the parsed document, give it the document's background color with a color fill, and attach it with `engine.block.appendChild(to:child:)`. ```swift highlight-createCustomImporter-scene let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: design.width) try engine.block.setHeight(page, value: design.height) if let background = design.background, background.count == 4 { let pageFill = try engine.block.createFill(.color) try engine.block.setColor( pageFill, property: "fill/color/value", color: .rgba(r: background[0], g: background[1], b: background[2], a: background[3]), ) try engine.block.setFill(page, fill: pageFill) } try engine.block.appendChild(to: scene, child: page) ``` ## Map Elements to Blocks Walk the parsed elements and create one block per element, then set its frame from the source coordinates and append it to the page. This loop is the core of any importer — only the per-type mapping changes: - **Image** → a graphic block with an image fill. Create the block with `engine.block.create(.graphic)`, give it a shape with `setShape`, build an image fill with `createFill(.image)`, and point it at the resolved URL with `setURL`. Image references resolve against the base URL where your importer's assets live. - **Rectangle** → a graphic block with a color fill. Build the fill with `createFill(.color)` and set `setColor(_:property:color:)`. - **Text** → a text block. Set its content with `replaceText` and let its height auto-size with `setHeightMode(_:mode: .auto)`. ```swift highlight-createCustomImporter-mapElements for element in design.elements { let block: DesignBlockID switch element.type { case .image: block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.image) if let src = element.src { try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent(src)) } try engine.block.setFill(block, fill: fill) case .rectangle: block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let fill = try engine.block.createFill(.color) if let color = element.color, color.count == 4 { try engine.block.setColor( fill, property: "fill/color/value", color: .rgba(r: color[0], g: color[1], b: color[2], a: color[3]), ) } try engine.block.setFill(block, fill: fill) case .text: block = try engine.block.create(.text) try engine.block.replaceText(block, text: element.text ?? "") try engine.block.setHeightMode(block, mode: .auto) } try engine.block.setPositionX(block, value: element.x) try engine.block.setPositionY(block, value: element.y) try engine.block.setWidth(block, value: element.width) // Text auto-sizes its height; every other element takes the source height. if element.type != .text { try engine.block.setHeight(block, value: element.height) } try engine.block.appendChild(to: page, child: block) } ``` Every block is positioned and sized in CE.SDK's top-left design units, so map your source's coordinate origin accordingly. ## Fit and Verify the Result Frame the page with `engine.scene.enableZoomAutoFit(_:axis:...)` so the import is visible, then confirm it is well-formed with `engine.scene.getPages()`. An empty result means the source produced no blocks. ```swift highlight-createCustomImporter-fitVerify try engine.scene.enableZoomAutoFit( page, axis: .both, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) let pages = try engine.scene.getPages() print("Imported design has \(pages.count) page(s)") ``` The imported design is now the active scene, fully editable — every element behaves like a natively created block. ## What Does Not Translate A custom importer reproduces only what you map. Set expectations accordingly: - Source features without a CE.SDK equivalent — custom filters, effects, blend modes — need explicit mapping or are dropped. - Fonts referenced by the source must be available to the engine, or text falls back to a default typeface. - Image elements resolve their URLs when the scene renders — an unreachable URL renders empty even though the block exists. - Centered origins or percentage units must be converted to top-left design units during the build step. ## Troubleshooting - **Imported blocks are invisible** — confirm each block was appended to the page and has a non-zero width and height. A graphic block also needs a shape and a fill to render. - **Image elements render empty** — verify the resolved `fill/image/imageFileURI` URL is reachable and the format is supported. - **Everything stacks at the top-left** — the source coordinates were not mapped; set `setPositionX` / `setPositionY` per element. - **Text shows a fallback font** — register or load the source's font, or accept the substitution. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.create()` | Create an empty design scene | | `engine.block.create(_:)` | Create a block of a `DesignBlockType` (`.page`, `.graphic`, `.text`) | | `engine.block.createShape(_:)` | Create a shape (`.rect`) for a graphic block | | `engine.block.setShape(_:shape:)` | Attach a shape to a graphic block | | `engine.block.createFill(_:)` | Create an image or color fill | | `engine.block.setURL(_:property:value:)` | Set the image fill URI (`fill/image/imageFileURI`) | | `engine.block.setColor(_:property:color:)` | Set the fill color (`fill/color/value`) | | `engine.block.setFill(_:fill:)` | Attach a fill to a block | | `engine.block.replaceText(_:text:)` | Set a text block's content | | `engine.block.setHeightMode(_:mode:)` | Set the height sizing mode (`.auto` for text) | | `engine.block.setWidth(_:value:)` / `setHeight(_:value:)` | Size a block in design units | | `engine.block.setPositionX(_:value:)` / `setPositionY(_:value:)` | Position a block in design units | | `engine.block.appendChild(to:child:)` | Add a child block to a parent | | `engine.scene.enableZoomAutoFit(_:axis:paddingLeft:paddingTop:paddingRight:paddingBottom:)` | Fit a block in the viewport | | `engine.scene.getPages()` | Return the scene's pages | ## Next Steps - [Import a Design](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design-73b9c5/) — Load native CE.SDK scenes, archives, images, and videos. - [From Photoshop](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-photoshop-cca6bb/) — Convert PSD files to a scene archive and load it. - [From InDesign](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-indesign-ba3988/) — Convert IDML files to a scene archive and load 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: "Using Default Assets" description: "Load shapes, stickers, images, and other built-in assets from IMG.LY's CDN to populate your CE.SDK editor using the Asset API." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/default-assets-d2763d/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Using Default Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/default-assets-d2763d/) --- ```swift file=@cesdk_swift_examples/engine-guides-default-assets/DefaultAssets.swift reference-only import Foundation import IMGLYEngine @MainActor func defaultAssets(engine: Engine) async throws { // Demo scaffolding: resolve sample assets against the engine's configured base // URL, with a wide page to host the three blocks the hero shows. let baseURL = try engine.guidesBaseURL let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 900) try engine.block.setHeight(page, value: 400) try engine.block.appendChild(to: scene, child: page) // Register a default asset source by loading its `content.json`. The returned // ID matches the source's `id` field in the JSON. let shapeSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.vector.shape/content.json"), ) let stickerSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.sticker/content.json"), ) // Demo asset sources — sample images, videos, and audio — load the same way. let imageSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.image/content.json"), ) // Fetch a specific asset by its ID, then apply it. // `apply(sourceID:assetResult:)` creates a block from the asset, attaches it // to the current page, and returns the new block's handle. guard let starAsset = try await engine.asset.fetchAsset( sourceID: shapeSourceID, assetID: "ly.img.vector.shape.filled.star", ), let starBlock = try await engine.asset.apply(sourceID: shapeSourceID, assetResult: starAsset), let emojiAsset = try await engine.asset.fetchAsset( sourceID: stickerSourceID, assetID: "ly.img.sticker.emoji.happyface", ), let emojiBlock = try await engine.asset.apply(sourceID: stickerSourceID, assetResult: emojiAsset), let imageAsset = try await engine.asset.fetchAsset( sourceID: imageSourceID, assetID: "ly.img.image.sample_1", ), let imageBlock = try await engine.asset.apply(sourceID: imageSourceID, assetResult: imageAsset) else { return } // Demo scaffolding: give the star a solid fill, keep the emoji uncropped, then // size and lay out the three blocks in a centered row for the hero. let starFill = try engine.block.createFill(.color) try engine.block.setColor( starFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.78, b: 0.0, a: 1.0), ) try engine.block.setFill(starBlock, fill: starFill) if try engine.block.supportsContentFillMode(emojiBlock) { try engine.block.setContentFillMode(emojiBlock, mode: .contain) } let blockSize: Float = 220 let spacing: Float = 50 let blocks = [starBlock, emojiBlock, imageBlock] let rowWidth = Float(blocks.count) * blockSize + Float(blocks.count - 1) * spacing let startX = (900 - rowWidth) / 2 for (index, block) in blocks.enumerated() { try engine.block.setWidth(block, value: blockSize) try engine.block.setHeight(block, value: blockSize) try engine.block.setPositionX(block, value: startX + Float(index) * (blockSize + spacing)) try engine.block.setPositionY(block, value: (400 - blockSize) / 2) } try await engine.captureGuide(page, label: "hero") } // Compile-only demonstration of the `matcher` parameter. The guide test does not // run this function: re-registering an asset source ID that is already loaded in // `defaultAssets(engine:)` would fail because source IDs must be unique. @MainActor func defaultAssetsWithMatcher(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL // Load only star and arrow shapes. let shapeSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.vector.shape/content.json"), matcher: ["*star*", "*arrow*"], ) // Load only emoji stickers. let stickerSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.sticker/content.json"), matcher: ["*emoji*"], ) print("Loaded filtered sources: \(shapeSourceID), \(stickerSourceID)") } ``` Load CE.SDK's built-in asset sources — shapes, stickers, filters, fonts, and sample media — from IMG.LY's CDN, then create blocks from them with the Asset API. ![A gold star shape, a happy-face emoji sticker, and a sample image laid out in a row on the canvas.](./assets/swift-based.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-default-assets) CE.SDK provides built-in asset sources for shapes, stickers, filters, effects, fonts, and sample media. This guide registers asset sources from IMG.LY's CDN and applies them to create a scene with a star shape, an emoji sticker, and an image. > **Production Deployment:** The IMG.LY CDN is for development and prototyping only. For production, download and self-host the assets from your own server. See the [Serve Assets](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) guide for instructions. ## What Are Default and Demo Assets? IMG.LY hosts two categories of asset sources on its CDN for development and prototyping. **Default Assets** are core editor components: | Source ID | Description | | --- | --- | | `ly.img.sticker` | Stickers: emojis, emoticons, decorations | | `ly.img.vector.shape` | Shapes: stars, arrows, polygons | | `ly.img.filter` | LUT and duotone color filters | | `ly.img.color.palette` | Default color palette | | `ly.img.effect` | Visual effects | | `ly.img.blur` | Blur effects | | `ly.img.typeface` | Font families | | `ly.img.crop.presets` | Crop presets | | `ly.img.page.presets` | Page size presets | | `ly.img.text`, `ly.img.text.styles`, `ly.img.text.curves` | Text style presets | | `ly.img.text.components` | Text design component library | **Demo Assets** are sample content: | Source ID | Description | | --- | --- | | `ly.img.image` | Sample images | | `ly.img.video` | Sample videos | | `ly.img.audio` | Sample audio tracks | | `ly.img.templates` | Design and video templates | | `ly.img.templates.premium` | Premium design templates | ## Loading Default Asset Sources Each asset source is described by a `content.json` manifest. Register a source by pointing `addLocalAssetSourceFromJSON(_:matcher:)` at its manifest URL; the engine resolves the asset files relative to that URL and returns the source ID declared in the JSON. Here `baseURL` points at the host serving your assets — the IMG.LY CDN during development. ```swift highlight-defaultAssets-loadDefault // Register a default asset source by loading its `content.json`. The returned // ID matches the source's `id` field in the JSON. let shapeSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.vector.shape/content.json"), ) let stickerSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.sticker/content.json"), ) ``` ## Loading Demo Asset Sources Demo asset sources — sample images, videos, and audio — register exactly the same way. ```swift highlight-defaultAssets-loadDemo // Demo asset sources — sample images, videos, and audio — load the same way. let imageSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.image/content.json"), ) ``` ## Creating Blocks from Assets Once a source is registered, fetch a specific asset with `fetchAsset(sourceID:assetID:)`, then pass the result to `apply(sourceID:assetResult:)`. `apply` creates a block from the asset's metadata, attaches it to the current page, and returns the new block's handle. ```swift highlight-defaultAssets-createBlocks // Fetch a specific asset by its ID, then apply it. // `apply(sourceID:assetResult:)` creates a block from the asset, attaches it // to the current page, and returns the new block's handle. guard let starAsset = try await engine.asset.fetchAsset( sourceID: shapeSourceID, assetID: "ly.img.vector.shape.filled.star", ), let starBlock = try await engine.asset.apply(sourceID: shapeSourceID, assetResult: starAsset), let emojiAsset = try await engine.asset.fetchAsset( sourceID: stickerSourceID, assetID: "ly.img.sticker.emoji.happyface", ), let emojiBlock = try await engine.asset.apply(sourceID: stickerSourceID, assetResult: emojiAsset), let imageAsset = try await engine.asset.fetchAsset( sourceID: imageSourceID, assetID: "ly.img.image.sample_1", ), let imageBlock = try await engine.asset.apply(sourceID: imageSourceID, assetResult: imageAsset) else { return } ``` `fetchAsset(sourceID:assetID:)` is the right call when you already know an asset's ID. To search or page through a source instead — for example, to build your own picker — use `findAssets(sourceID:query:)` with an `AssetQueryData` (a search string, group filters, and pagination), which returns an `AssetQueryResult` of matching assets. On iOS, the editor's asset library populates itself with these queries so users can browse and select assets, rather than fetching by explicit ID; see the [Asset Library Basics](#broken-link-f29078) guide. ## Filtering Assets with Matcher Pass a `matcher` array to load only the assets whose IDs match. An asset is included if it matches any pattern, and `*` is a wildcard. Because a source ID must be unique, apply the matcher when you first register the source rather than re-registering an already-loaded one. ```swift highlight-defaultAssets-matcher // Load only star and arrow shapes. let shapeSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.vector.shape/content.json"), matcher: ["*star*", "*arrow*"], ) // Load only emoji stickers. let stickerSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.sticker/content.json"), matcher: ["*emoji*"], ) print("Loaded filtered sources: \(shapeSourceID), \(stickerSourceID)") ``` ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addLocalAssetSourceFromJSON(_:matcher:)` | Register an asset source by loading its `content.json` manifest from a URL. Pass `matcher` ID patterns to filter which assets load. Returns the source ID. | | `engine.asset.addLocalAssetSourceFromJSON(_:basePath:matcher:)` | Register an asset source from an in-memory JSON string, resolving relative URLs against `basePath`. | | `engine.asset.fetchAsset(sourceID:assetID:)` | Fetch a single asset from a source by its ID. | | `engine.asset.apply(sourceID:assetResult:)` | Create a block from an asset and add it to the scene. Returns the new block's handle. | ## Next Steps - [Serve Assets](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) — Self-host assets for production deployments. - [Customize Asset Library](#broken-link-c9a4de) — On iOS, configure the asset library UI and entries. - [Asset Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) — Understand asset sources and how they organize content. - [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) — Connect CE.SDK to external sources like servers or third-party platforms to import assets remotely. --- ## 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 Assets" description: "Manage assets in local asset sources by updating metadata, removing individual assets, or deleting entire sources in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/edit-or-remove-assets-ce072c/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Edit or Remove Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/edit-or-remove-assets-ce072c/) --- ```swift file=@cesdk_swift_examples/engine-guides-edit-or-remove-assets/EditOrRemoveAssets.swift reference-only import Foundation import IMGLYEngine @MainActor func editOrRemoveAssets(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL try engine.asset.addLocalSource(sourceID: "my-uploads") let mountainURI = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg").absoluteString let mountainThumbURI = baseURL.appendingPathComponent("ly.img.image/thumbnails/sample_1.jpg").absoluteString try engine.asset.addAsset(to: "my-uploads", asset: AssetDefinition( id: "image-1", meta: [ "uri": mountainURI, "thumbUri": mountainThumbURI, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Mountain Landscape"], tags: ["en": ["nature", "mountain"]], )) let oceanURI = baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg").absoluteString let oceanThumbURI = baseURL.appendingPathComponent("ly.img.image/thumbnails/sample_2.jpg").absoluteString try engine.asset.addAsset(to: "my-uploads", asset: AssetDefinition( id: "image-2", meta: [ "uri": oceanURI, "thumbUri": oceanThumbURI, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Ocean Waves"], tags: ["en": ["nature", "water"]], )) let forestURI = baseURL.appendingPathComponent("ly.img.image/images/sample_3.jpg").absoluteString let forestThumbURI = baseURL.appendingPathComponent("ly.img.image/thumbnails/sample_3.jpg").absoluteString try engine.asset.addAsset(to: "my-uploads", asset: AssetDefinition( id: "image-3", meta: [ "uri": forestURI, "thumbUri": forestThumbURI, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Forest Path"], tags: ["en": ["nature", "forest"]], )) let result = try await engine.asset.findAssets( sourceID: "my-uploads", query: .init(query: "nature", page: 0, perPage: 100), ) let assetToModify = result.assets.first { $0.id == "image-1" } print("Found \(result.total) assets; editing \(assetToModify?.label ?? "none")") try engine.asset.removeAsset(from: "my-uploads", assetID: "image-1") try engine.asset.addAsset(to: "my-uploads", asset: AssetDefinition( id: "image-1", meta: [ "uri": mountainURI, "thumbUri": mountainThumbURI, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Updated Mountain Photo"], tags: ["en": ["nature", "mountain", "updated"]], )) try engine.asset.removeAsset(from: "my-uploads", assetID: "image-2") try engine.asset.assetSourceContentsChanged(sourceID: "my-uploads") let temporaryURI = baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg").absoluteString let temporaryThumbURI = baseURL.appendingPathComponent("ly.img.image/thumbnails/sample_4.jpg").absoluteString try engine.asset.addLocalSource(sourceID: "temporary-uploads") try engine.asset.addAsset(to: "temporary-uploads", asset: AssetDefinition( id: "temp-1", meta: [ "uri": temporaryURI, "thumbUri": temporaryThumbURI, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Temporary Image"], )) try engine.asset.removeSource(sourceID: "temporary-uploads") let addedListener = Task { for await sourceID in engine.asset.onAssetSourceAdded { print("Source added: \(sourceID)") } } let removedListener = Task { for await sourceID in engine.asset.onAssetSourceRemoved { print("Source removed: \(sourceID)") } } let updatedListener = Task { for await sourceID in engine.asset.onAssetSourceUpdated { print("Source updated: \(sourceID)") } } try engine.asset.addLocalSource(sourceID: "event-demo-source") try engine.asset.assetSourceContentsChanged(sourceID: "event-demo-source") try engine.asset.removeSource(sourceID: "event-demo-source") addedListener.cancel() removedListener.cancel() updatedListener.cancel() } ``` Manage assets in local asset sources by updating metadata, removing individual assets, or deleting entire sources. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-edit-or-remove-assets) Assets in local sources can be modified or removed after they have been added. CE.SDK provides two levels of removal: individual assets within a source and entire asset sources. This guide covers how to query, update, and remove assets programmatically, and how to notify the engine when a source's contents change. Adding assets to a source is covered in the [Local Asset](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-local-source/local-asset-3f93f2/) guide, and on iOS in the [User Upload](#broken-link-c6c7d9) guide. ## Creating a Local Asset Source To demonstrate editing and removal, first register a local source and populate it. Use `addLocalSource(sourceID:)` to create the source, then `addAsset(to:asset:)` for each asset. ```swift highlight-editOrRemoveAssets-createSource try engine.asset.addLocalSource(sourceID: "my-uploads") let mountainURI = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg").absoluteString let mountainThumbURI = baseURL.appendingPathComponent("ly.img.image/thumbnails/sample_1.jpg").absoluteString try engine.asset.addAsset(to: "my-uploads", asset: AssetDefinition( id: "image-1", meta: [ "uri": mountainURI, "thumbUri": mountainThumbURI, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Mountain Landscape"], tags: ["en": ["nature", "mountain"]], )) let oceanURI = baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg").absoluteString let oceanThumbURI = baseURL.appendingPathComponent("ly.img.image/thumbnails/sample_2.jpg").absoluteString try engine.asset.addAsset(to: "my-uploads", asset: AssetDefinition( id: "image-2", meta: [ "uri": oceanURI, "thumbUri": oceanThumbURI, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Ocean Waves"], tags: ["en": ["nature", "water"]], )) let forestURI = baseURL.appendingPathComponent("ly.img.image/images/sample_3.jpg").absoluteString let forestThumbURI = baseURL.appendingPathComponent("ly.img.image/thumbnails/sample_3.jpg").absoluteString try engine.asset.addAsset(to: "my-uploads", asset: AssetDefinition( id: "image-3", meta: [ "uri": forestURI, "thumbUri": forestThumbURI, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Forest Path"], tags: ["en": ["nature", "forest"]], )) ``` Each asset has a unique `id`, a localized `label` and `tags` for searchability, and a `meta` dictionary that holds the asset's `uri`, `thumbUri`, and `fillType`. Point `thumbUri` at a small preview image so the asset library loads quickly, separate from the full-resolution `uri`. ## Finding Assets in a Source Use `findAssets(sourceID:query:)` to query a source. `AssetQueryData` takes a fuzzy `query` string that matches labels and tags, plus `page` and `perPage` for pagination. ```swift highlight-editOrRemoveAssets-findAssets let result = try await engine.asset.findAssets( sourceID: "my-uploads", query: .init(query: "nature", page: 0, perPage: 100), ) let assetToModify = result.assets.first { $0.id == "image-1" } print("Found \(result.total) assets; editing \(assetToModify?.label ?? "none")") ``` The call returns an `AssetQueryResult` with the `assets` array, the `total` count, `currentPage`, and `nextPage`. Filter the results by `id` to locate the specific asset you want to edit or remove. ## Updating Asset Metadata The Asset API has no in-place update. To change an asset's metadata — labels, tags, or `uri` — remove the existing asset and add a new version that keeps the same `id`. ```swift highlight-editOrRemoveAssets-updateMetadata try engine.asset.removeAsset(from: "my-uploads", assetID: "image-1") try engine.asset.addAsset(to: "my-uploads", asset: AssetDefinition( id: "image-1", meta: [ "uri": mountainURI, "thumbUri": mountainThumbURI, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Updated Mountain Photo"], tags: ["en": ["nature", "mountain", "updated"]], )) ``` Reusing the same `id` keeps references to the asset valid while the label, tags, and other metadata reflect the new values. ## Removing an Asset from a Source Remove a single asset with `removeAsset(from:assetID:)`. The asset is permanently deleted from the source, but blocks already created from it remain on the canvas — removal only affects the source's contents. ```swift highlight-editOrRemoveAssets-removeAsset try engine.asset.removeAsset(from: "my-uploads", assetID: "image-2") ``` ## Notifying of Changes After modifying a source, call `assetSourceContentsChanged(sourceID:)`. This emits an update event that subscribers — including the editor UI when present — use to refresh the assets they display. ```swift highlight-editOrRemoveAssets-notifyUI try engine.asset.assetSourceContentsChanged(sourceID: "my-uploads") ``` ## Creating a Temporary Source Register additional sources for temporary or session-specific assets that you can remove entirely when they are no longer needed. ```swift highlight-editOrRemoveAssets-createTempSource let temporaryURI = baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg").absoluteString let temporaryThumbURI = baseURL.appendingPathComponent("ly.img.image/thumbnails/sample_4.jpg").absoluteString try engine.asset.addLocalSource(sourceID: "temporary-uploads") try engine.asset.addAsset(to: "temporary-uploads", asset: AssetDefinition( id: "temp-1", meta: [ "uri": temporaryURI, "thumbUri": temporaryThumbURI, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Temporary Image"], )) ``` ## Removing an Entire Asset Source Remove a complete source and all of its assets with `removeSource(sourceID:)`. Any UI displaying the source stops showing its content. ```swift highlight-editOrRemoveAssets-removeSource try engine.asset.removeSource(sourceID: "temporary-uploads") ``` Use this when cleaning up temporary sources or when a user deletes an entire category of imported media. ## Listening to Asset Source Events Subscribe to source lifecycle events to react when sources are added, removed, or have their contents updated — useful for analytics, cleanup, or syncing with external systems. Each event is exposed as an `AsyncStream` of source IDs. ```swift highlight-editOrRemoveAssets-events let addedListener = Task { for await sourceID in engine.asset.onAssetSourceAdded { print("Source added: \(sourceID)") } } let removedListener = Task { for await sourceID in engine.asset.onAssetSourceRemoved { print("Source removed: \(sourceID)") } } let updatedListener = Task { for await sourceID in engine.asset.onAssetSourceUpdated { print("Source updated: \(sourceID)") } } try engine.asset.addLocalSource(sourceID: "event-demo-source") try engine.asset.assetSourceContentsChanged(sourceID: "event-demo-source") try engine.asset.removeSource(sourceID: "event-demo-source") addedListener.cancel() removedListener.cancel() updatedListener.cancel() ``` Iterate each stream inside a `Task`. Cancelling the `Task` unsubscribes, the equivalent of disposing the subscription when you no longer need events. ## Best Practices - **Query before modifying** — Use `findAssets(sourceID:query:)` to confirm an asset exists before removing it. - **Notify after changes** — Call `assetSourceContentsChanged(sourceID:)` so the UI reflects edits and removals. - **Cancel subscriptions** — Store the listening `Task`s and cancel them when you no longer need events to avoid leaks. - **Reuse IDs to update** — Re-add an asset with the same `id` so existing references keep working after the update. - **Use descriptive IDs** — Choose unique, meaningful asset IDs for reliable lookups. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Asset not found | ID mismatch or already removed | Query the source with `findAssets(sourceID:query:)` to list available assets | | UI shows stale assets | Missing notification | Call `assetSourceContentsChanged(sourceID:)` after modifying the source | | Cannot remove asset | Source is not a local source | Only sources created with `addLocalSource(sourceID:)` support asset removal | | Events never fire | Listener cancelled or started too late | Start the listening `Task` before triggering the operation | ## API Reference ### Methods | Method | Description | | --- | --- | | `addLocalSource(sourceID:)` | Register a local asset source you can add to and remove from | | `addAsset(to:asset:)` | Add an asset to a local source | | `findAssets(sourceID:query:)` | Query a source's assets with filtering and pagination | | `removeAsset(from:assetID:)` | Remove a single asset from a local source | | `removeSource(sourceID:)` | Remove an entire source and all of its assets | | `assetSourceContentsChanged(sourceID:)` | Notify subscribers that a source's contents changed | | `onAssetSourceAdded` | `AsyncStream` of IDs for sources as they are added | | `onAssetSourceRemoved` | `AsyncStream` of IDs for sources as they are removed | | `onAssetSourceUpdated` | `AsyncStream` of IDs for sources whose contents changed | ### Properties | Property | Type | Description | | --- | --- | --- | | `uri` | String | URL of the asset's full-resolution file | | `thumbUri` | String | URL of the asset's thumbnail preview | | `fillType` | String | Fill applied when the asset is inserted (`//ly.img.ubq/fill/image` for images) | --- ## 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 File Formats for Import" description: "Review the supported image, video, audio, and template formats for importing assets into CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/file-format-support-8cdc84/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [File Format Support](https://img.ly/docs/cesdk/mac-catalyst/import-media/file-format-support-8cdc84/) --- When building creative applications with CE.SDK, understanding which file formats your users can import is crucial for delivering a smooth editing experience. CE.SDK supports a comprehensive range of modern media formats. This guide provides a complete reference of supported file formats for importing media, templates, and fonts into CE.SDK. ## Supported Import Formats CE.SDK supports importing the following media types: ## Video and Audio Codecs While container formats (`.mp4`, `.mov`) define how media is packaged, codecs determine how the content is compressed. CE.SDK supports the following codecs for playback and editing: > **Warning:** H.265/HEVC playback depends on the device providing a hardware or system > decoder. Availability varies across devices and operating system versions, so > test H.265 content on your target hardware before relying on it. ## Size Limits and Constraints CE.SDK processes media on-device, so performance is bounded by the hardware capabilities of the device running the editor. Keep these practical limits in mind: ### Image Resolution Limits ### Video Resolution and Duration Limits ## Format-Specific Considerations ### SVG Limitations ### WebP Support WebP images are fully supported for import. CE.SDK handles both lossy and lossless WebP formats, including images with transparency (alpha channel). WebP provides excellent compression with high quality, making it a strong choice for creative applications. ### Animated Image Considerations (GIF and APNG) CE.SDK handles animated GIF and APNG files based on scene type: - **Design scenes**: rendered as a static image showing the first frame. - **Video scenes**: imported as a looping video fill, with frame timing and duration parsed from the file's metadata. For vector-based animations, consider **Lottie** (`.json`). For complex animated content, prefer `.mp4`, which offers better compression and broad codec support. ### Template Format Details CE.SDK loads scenes and design archives directly on-device: - **Scene** – CE.SDK's native scene format (`.imgly` or `.scene`), loaded with `engine.scene.load(from:)`. - **Archive** – A portable `.imgly` (or `.zip`) file that bundles a scene with its embedded assets, loaded with the same `engine.scene.load(from:)` call. **PSD** (Adobe Photoshop) and **IDML** (Adobe InDesign) files have no on-device parser — the `@imgly/psd-importer` and `@imgly/idml-importer` packages parse them in Node.js or the browser. Convert those files to an `.imgly` archive on a server, then load the archive on-device with `engine.scene.load(from:)`. See [Import from Photoshop](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-photoshop-cca6bb/) and [Import from InDesign](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-indesign-ba3988/) for the complete workflow. ## Font Format Support CE.SDK supports modern font formats for typography: ## Best Practices ### Format Selection When building your application, consider these format recommendations: - **Images**: Use `.webp` for the best compression-to-quality ratio. Fall back to `.png` for transparency or `.jpeg` for photographs without transparency. - **Video**: Prefer `.mp4` with H.264 encoding for the widest compatibility across devices. - **Audio**: Use `.mp3` for universal compatibility or `.m4a` (AAC) for better quality at smaller file sizes. - **Templates**: Use `.imgly` scene or archive files for CE.SDK-to-CE.SDK workflows. To migrate `.psd`/`.idml` designs, convert them to an `.imgly` archive first — there is no on-device parser for those formats. ### Validation and Error Handling Always validate file formats before attempting import: 1. **Check MIME types** when a file is selected to quickly reject unsupported formats. 2. **Validate file extensions** as a first line of defense. 3. **Monitor file sizes** to prevent memory issues with extremely large files. 4. **Provide clear error messages** that explain which formats are supported when an import fails. ### Memory Management Media is processed on-device, so be mindful of memory constraints: - Large video files can cause performance issues on memory-constrained devices. - Multiple high-resolution images loaded simultaneously can exhaust GPU memory. - Consider lazy loading for asset libraries that contain many files. - Provide progress indicators for large file imports to improve the user experience. --- ## 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 From Local Source" description: "Enable users to upload files from their device for use as design assets in the editor." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/from-local-source-39b2a9/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Import From Local Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-local-source-39b2a9/) --- --- ## Related Pages - [Import Local Asset](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-local-source/local-asset-3f93f2/) - Import files directly from the user's device and insert them into the design 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: "Import Local Asset" description: "Import files directly from the user's device and insert them into the design canvas." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/from-local-source/local-asset-3f93f2/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Import From Local Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-local-source-39b2a9/) > [Import Local Asset](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-local-source/local-asset-3f93f2/) --- ```swift file=@cesdk_swift_examples/engine-guides-import-media-local-asset/ImportLocalAsset.swift reference-only import Foundation import IMGLYEngine @MainActor func importLocalAsset(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL try engine.asset.addLocalSource(sourceID: "my-local-images") try engine.asset.addLocalSource( sourceID: "my-local-audio", supportedMimeTypes: ["audio/mpeg", "audio/mp4"], ) // application bundle let bundledImageURLs = Bundle.main.urls( forResourcesWithExtension: "jpg", subdirectory: "SampleImages", ) ?? [] // Application Support directory let supportDirectory = try FileManager.default.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true, ) let managedImageURLs = try FileManager.default.contentsOfDirectory( at: supportDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles], ).filter { ["jpg", "jpeg", "png"].contains($0.pathExtension.lowercased()) } print("Located \(bundledImageURLs.count) bundled and \(managedImageURLs.count) managed images") let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") let thumbnailURL = baseURL.appendingPathComponent("ly.img.image/thumbnails/sample_1.jpg") let imageAsset = AssetDefinition( id: "local-image-1", meta: [ "uri": imageURL.absoluteString, "thumbUri": thumbnailURL.absoluteString, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Mountain Landscape"], tags: ["en": ["local", "nature", "mountain"]], ) try engine.asset.addAsset(to: "my-local-images", asset: imageAsset) let result = try await engine.asset.findAssets( sourceID: "my-local-images", query: .init(query: nil, page: 0, perPage: 10), ) print("The source now holds \(result.total) asset(s)") } ``` Register a local asset source, describe files that already live on the device as assets, and add them to the source so users can reuse them throughout your app. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-import-media-local-asset) A local asset source is a named repository you register with the engine. It owns a list of assets and, unlike a remote source, resolves every asset from a local file URL — an image bundled with your app, a file your app manages, or a file the user picked from disk. Once a file is registered as an asset, the engine indexes it, makes it searchable, and can insert it into a scene. This guide covers registering a source, turning a file URL into an asset, and adding it. Querying, updating, and removing assets are covered in the [Edit or Remove Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/edit-or-remove-assets-ce072c/) guide. ## Register a Local Asset Source Create a source with `addLocalSource(sourceID:)`. The `sourceID` must be unique, and you reuse it for every later operation on the source. ```swift highlight-importLocalAsset-register try engine.asset.addLocalSource(sourceID: "my-local-images") ``` A single source can hold images, videos, and audio together. To declare which types a source is meant to hold, pass `supportedMimeTypes`. The engine reports this list back through `getSupportedMIMETypes(sourceID:)`, which your own upload UI or validation code can read to decide which files to accept. Omit the parameter to leave the source open to every type. ```swift highlight-importLocalAsset-restrictMimeTypes try engine.asset.addLocalSource( sourceID: "my-local-audio", supportedMimeTypes: ["audio/mpeg", "audio/mp4"], ) ``` `addLocalSource` also accepts optional `applyAsset` and `applyAssetToBlock` callbacks that override how the engine inserts this source's assets onto the canvas; omit them, as here, to use the default insertion behavior. Whether you use one source or several is an organizational choice. A dedicated source per media type keeps unrelated assets apart; a single mixed source is simpler to manage. ## Locate Files on the Device Every asset needs a URL that points to the actual file. There are three regular places to keep local files, each with different lifetime and access guarantees: - The **application bundle**, for assets you ship with the app. These are read-only. - The **Application Support** directory, for files your app creates and manages on the user's behalf. This is the right home for editor-managed media. - The **Documents** directory, for user-facing files. Both you and the user have full control, so the user can move or delete them. ```swift highlight-importLocalAsset-locateFiles // application bundle let bundledImageURLs = Bundle.main.urls( forResourcesWithExtension: "jpg", subdirectory: "SampleImages", ) ?? [] // Application Support directory let supportDirectory = try FileManager.default.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true, ) let managedImageURLs = try FileManager.default.contentsOfDirectory( at: supportDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles], ).filter { ["jpg", "jpeg", "png"].contains($0.pathExtension.lowercased()) } print("Located \(bundledImageURLs.count) bundled and \(managedImageURLs.count) managed images") ``` > **Note:** The system can clear the temporary and caches directories at any time without asking the user. They are poor homes for source files but excellent for regenerable data such as thumbnails. A file picker is another common way to obtain a URL — forward the picked URL to the next step. On iOS, the editor's built-in upload and photo-library flows do this for you; see the [User Upload](#broken-link-c6c7d9) and [Photo Roll](#broken-link-23820d) guides. ## Describe an Asset An asset is a file URL plus metadata, wrapped in an `AssetDefinition`. The minimum is a unique `id` and a `meta` dictionary with a `uri`. For an image, add a `fillType` of `//ly.img.ubq/fill/image` so the engine applies an image fill when the asset is inserted, and a `thumbUri` pointing at a small preview so the asset library loads quickly, separate from the full-resolution `uri`. The example below uses a bundled sample image in place of one of the URLs you located above — such as `bundledImageURLs.first` — so it runs on its own; wire your own local file URL in exactly the same way. ```swift highlight-importLocalAsset-definition let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") let thumbnailURL = baseURL.appendingPathComponent("ly.img.image/thumbnails/sample_1.jpg") let imageAsset = AssetDefinition( id: "local-image-1", meta: [ "uri": imageURL.absoluteString, "thumbUri": thumbnailURL.absoluteString, "fillType": "//ly.img.ubq/fill/image", ], label: ["en": "Mountain Landscape"], tags: ["en": ["local", "nature", "mountain"]], ) ``` The localized `label` and `tags` make the asset findable through free-text search and describe it for accessibility. You can add more entries to `meta` — `width` and `height` set the asset's aspect ratio on insert, and `mimeType` records its format. A video asset should carry a `thumbUri` still image so it shows a proper preview, and both video and audio assets benefit from a `duration` entry. > **Caution:** An absolute file URL points into your app's container, whose location changes between installs and differs across devices and machines. A design saved with `saveToString()` keeps those URLs verbatim, so a scene that references locally imported files won't reload elsewhere. To persist such a scene, use `engine.scene.saveToArchive()` — it embeds the referenced files and rewrites their URLs to archive-relative paths. Use `relocateResource(currentURL:relocatedURL:)` to repoint a resource that has moved. ## Add the Asset to the Source Add the finished definition with `addAsset(to:asset:)`. The engine indexes it immediately, exposes it to search, and emits a source-changed event — so any surface already displaying the source, including the editor UI when present, refreshes on its own. You don't need to notify the engine after adding or removing an asset. ```swift highlight-importLocalAsset-add try engine.asset.addAsset(to: "my-local-images", asset: imageAsset) ``` Call `assetSourceContentsChanged(sourceID:)` yourself only when a source's contents change through a path the engine can't observe — for example, after you rewrite the backing files on disk or update a custom asset source's store outside `addAsset`/`removeAsset`. ## Verify the Source Contents Query the source with `findAssets(sourceID:query:)` to confirm the asset landed. `AssetQueryData` takes an optional fuzzy `query` string, plus `page` and `perPage` for pagination; the result reports the `total` count and the matching `assets`. ```swift highlight-importLocalAsset-verify let result = try await engine.asset.findAssets( sourceID: "my-local-images", query: .init(query: nil, page: 0, perPage: 10), ) print("The source now holds \(result.total) asset(s)") ``` ## Displaying Assets in the Editor On iOS, you surface a registered source in the prebuilt editor's Asset Panel by adding it to a tab. A tab renders every asset in its bound source with that tab's item view — the Images tab draws each asset as an image — so it does not filter a mixed source by type. The default library reflects this by backing each tab with a dedicated per-type source (`ly.img.image`, `ly.img.video`, `ly.img.audio`), so keep a source's contents matched to the tab it appears in and split assets across per-type sources when you need more than one type. To place a source in the panel and choose its tab, see the [Asset Library Basics](#broken-link-f29078) and [Customize Asset Library](#broken-link-c9a4de) guides. ## Best Practices - **Keep the `sourceID` stable** — Changing it after users have interacted with a source breaks search history and any saved references. - **Store managed files outside Documents** — Keep editor-managed media in Application Support so the user can't delete it out from under the app. - **Generate real thumbnails** — Point `thumbUri` at a small preview rather than the full-resolution file so the library stays responsive. - **Localize `label` and `tags`** — Free-text search matches on both fields, so populate them for every asset you want users to find. - **Notify only for out-of-band changes** — `addAsset` and `removeAsset` refresh the UI for you; call `assetSourceContentsChanged(sourceID:)` yourself only when a source's contents change outside those calls. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Asset shows a gray or error icon | Missing or invalid `uri`, or a missing `thumbUri` for a video asset | Confirm `uri` points at a readable file and set a valid `thumbUri` | | Assets disappear after relaunch | Files were stored only in Caches or a temporary directory | Copy files into Application Support (or your backend) and re-register them at launch | | A saved scene can't find its images elsewhere | The scene stored absolute app-container file URLs | Persist with `saveToArchive()`, which embeds the files and rewrites their URLs to relative paths | | Search doesn't find an asset | Empty `label` and `tags` | Populate localized `label` and `tags` in the `AssetDefinition` | | Adding an asset throws | An asset with the same `id` already exists in the source | Use a unique `id`, or remove the existing asset before re-adding it | ## API Reference ### Methods | Method | Description | | --- | --- | | `addLocalSource(sourceID:supportedMimeTypes:applyAsset:applyAssetToBlock:)` | Register a local asset source. Every parameter after `sourceID` is optional: `supportedMimeTypes` declares which types the source holds; `applyAsset` / `applyAssetToBlock` override how the engine inserts its assets. | | `addAsset(to:asset:)` | Add an asset to a local source | | `getSupportedMIMETypes(sourceID:)` | Read back the MIME types a source declares (empty means all types) | | `assetSourceContentsChanged(sourceID:)` | Notify subscribers that a source's contents changed | | `findAssets(sourceID:query:)` | Query a source's assets with filtering and pagination | ### Meta Keys These are well-known keys the engine reads from the `AssetDefinition.meta` dictionary — not Swift properties on `AssetDefinition` (whose properties are `id`, `groups`, `meta`, `payload`, `label`, and `tags`). | Key | Type | Description | | --- | --- | --- | | `uri` | String | URL of the asset's file | | `thumbUri` | String | URL of the asset's thumbnail preview | | `fillType` | String | Fill applied when the asset is inserted (`//ly.img.ubq/fill/image` for images) | ## Next Steps - [Edit or Remove Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/edit-or-remove-assets-ce072c/) — Query, update, and remove assets or entire sources. - [User Upload](#broken-link-c6c7d9) — On iOS, let users add files through the editor's upload button. - [Photo Roll](#broken-link-23820d) — On iOS, import photos directly from the device's photo library. - [Import from a Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) — Connect the editor to a server or third-party service. --- ## 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 From Remote Source" description: "Connect CE.SDK to external sources like servers or third-party platforms to import assets remotely." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) --- --- ## Related Pages - [Import Remote Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/remote-asset-484685/) - Load asset definitions from remote JSON files hosted on CDNs or servers into CE.SDK's asset library with filtering and custom base URLs. - [From Your Server](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/your-server-b91910/) - Serve images, videos, audio, and stickers from your own backend into CE.SDK by implementing a custom asset source or loading a JSON manifest. - [Integrate IMG.LY Premium Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/imgly-premium-assets-eb1688/) - Host IMG.LY premium templates on your own infrastructure and integrate them into CE.SDK's asset library. - [Integrate Third-Party APIs](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/third-party-a05854/) - Import media from any third-party service into CE.SDK using custom asset sources. - [Integrate Unsplash Stock Images](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/) - Search and import high-quality stock images from Unsplash directly into CE.SDK. - [Integrate Pexels Stock Images](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/pexels-90d5df/) - Search and import high-quality royalty-free stock photos from Pexels directly into CE.SDK. - [Integrate Getty Images Stock Photos](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/getty-images-a3931c/) - Search and import premium stock photography from Getty Images directly into CE.SDK using a secure proxy server. - [Versioning of Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/asset-versioning-0a4d58/) - Learn how CE.SDK handles asset URLs in saved designs, including strategies for managing URL changes, migrating assets, and choosing between scene serialization and archive exports. --- ## 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: "Versioning of Assets" description: "Learn how CE.SDK handles asset URLs in saved designs, including strategies for managing URL changes, migrating assets, and choosing between scene serialization and archive exports." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/asset-versioning-0a4d58/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) > [Versioning of Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/asset-versioning-0a4d58/) --- ```swift file=@cesdk_swift_examples/engine-guides-import-media-from-remote-source-asset-versioning/AssetVersioning.swift reference-only import Foundation import IMGLYEngine @MainActor func assetVersioning(engine: Engine) async throws { // Demo scaffolding: build a small scene with a single image block so every // snippet below has an asset URL to inspect and update. In your app the scene // is already loaded and its blocks already exist. let baseURL = try engine.guidesBaseURL 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: 800) try engine.block.setHeight(page, value: 600) let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") // An asset reference lives on a fill block. Create a graphic, give it an image // fill, and store the asset URL in the fill's `fill/image/imageFileURI` property. 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.appendChild(to: page, child: imageBlock) // Read the URL back — this is exactly the string a saved scene serializes. let storedURL = try engine.block.getURL(imageFill, property: "fill/image/imageFileURI") print("Stored image URL:", storedURL.absoluteString) // Load the image so the archive below can embed its bytes. try await engine.block.forceLoadResources([imageBlock]) // `saveToString` serializes the scene structure and keeps asset references as // URLs. The result is small but depends on those URLs staying reachable. let sceneString = try await engine.scene.saveToString() _ = sceneString // `saveToArchive` bundles the scene together with the bytes of every reachable // asset into a self-contained archive you can write to disk with `write(to:)`. let archiveData = try await engine.scene.saveToArchive() _ = archiveData // Point a fill at a new asset URL — for example, after moving assets to a new // CDN or publishing a new version of an image. let migratedURL = URL(string: "https://cdn.example.com/assets/v2/product-photo.jpg")! try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: migratedURL) let updatedURL = try engine.block.getURL(imageFill, property: "fill/image/imageFileURI") print("Updated image URL:", updatedURL.absoluteString) // To migrate assets in bulk, walk every graphic block and rewrite the fills // that carry an image. Filter by fill type, since a graphic block can hold a // color, gradient, image, or video fill. let graphicBlocks = try engine.block.find(byType: .graphic) for block in graphicBlocks { let fill = try engine.block.getFill(block) guard try engine.block.getType(fill) == FillType.image.rawValue else { continue } let currentURL = try engine.block.getURL(fill, property: "fill/image/imageFileURI") // Rewrite only the assets hosted on the CDN you are retiring. if currentURL.absoluteString.contains("old-cdn.example.com") { let rewritten = currentURL.absoluteString.replacingOccurrences( of: "old-cdn.example.com", with: "new-cdn.example.com", ) if let newURL = URL(string: rewritten) { try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: newURL) } } } } ``` Manage how CE.SDK stores and resolves asset URLs in saved designs, keeping designs functional when assets are updated or moved. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-import-media-from-remote-source-asset-versioning) CE.SDK references assets via URLs rather than embedding files directly into designs. When you save a design with `engine.scene.saveToString()`, asset URLs are stored as strings. On load, CE.SDK fetches assets from those URLs. This keeps saved designs small but means URL changes can break existing designs. This guide explains how CE.SDK stores asset references and strategies for managing asset URLs over time. This guide covers how to inspect asset URLs stored in designs, the difference between scene serialization and archive export, how to programmatically update asset URLs, and strategies for versioned URL schemes. ## How Asset URLs Are Stored An asset reference lives on a fill block, not on the graphic block itself. When you add an image to a design, you create an image fill that stores the source URL in its `fill/image/imageFileURI` property. Use `getFill(_:)` to reach the fill and `getURL(_:property:)` to inspect the stored URL. Here `imageURL` is the URL of the image you want to display and `page` is a page in your scene. ```swift highlight-assetVersioning-storeURI // An asset reference lives on a fill block. Create a graphic, give it an image // fill, and store the asset URL in the fill's `fill/image/imageFileURI` property. 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.appendChild(to: page, child: imageBlock) // Read the URL back — this is exactly the string a saved scene serializes. let storedURL = try engine.block.getURL(imageFill, property: "fill/image/imageFileURI") print("Stored image URL:", storedURL.absoluteString) ``` The `fill/image/imageFileURI` property contains exactly what gets written to the saved scene. CE.SDK doesn't transform or normalize these URLs — they're stored and loaded as-is. ## Scene Serialization vs Archive Export CE.SDK provides two approaches for saving designs, each with different trade-offs for asset handling. ### Saving as a Scene String The `saveToString()` method serializes the scene structure while keeping asset references as URLs. This produces a small `String` that loads quickly, but requires the original assets to remain available at their URLs. Persist the returned value wherever you store designs. ```swift highlight-assetVersioning-saveScene // `saveToString` serializes the scene structure and keeps asset references as // URLs. The result is small but depends on those URLs staying reachable. let sceneString = try await engine.scene.saveToString() ``` Use scene strings when: - Assets are hosted on a stable CDN with reliable URLs - You want to keep storage costs low - Designs need to load quickly - You can guarantee asset availability ### Saving as an Archive The `saveToArchive()` method bundles the scene together with all referenced assets into a self-contained archive. It returns the archive as `Data`, which you write to disk with `write(to:)`. Archives work without network access because every asset is embedded. ```swift highlight-assetVersioning-saveArchive // `saveToArchive` bundles the scene together with the bytes of every reachable // asset into a self-contained archive you can write to disk with `write(to:)`. let archiveData = try await engine.scene.saveToArchive() ``` Use archives when: - Designs need to work offline - You're migrating designs between environments - You can't guarantee long-term URL availability - Portability is more important than file size | Approach | Method | Assets | File Size | Portability | |----------|--------|--------|-----------|-------------| | Scene | `saveToString()` | Referenced by URL | Small | Requires URL availability | | Archive | `saveToArchive()` | Embedded in archive | Larger | Self-contained | ## What Happens When URLs Change When a design is loaded and an asset URL returns a 404 or is otherwise unavailable, the block appears empty or shows an error state. A previously fetched copy of the asset may still appear until caches expire, which can temporarily mask a broken URL. CE.SDK doesn't provide automatic fallbacks or retries for failed asset loads. If some assets fail while others succeed, the design loads partially. To prevent broken designs, ensure assets remain available at their original URLs or migrate designs when URLs change. ## Updating Asset URLs Programmatically When you need to migrate assets to a new location, load the existing scene, update the URLs, and save the modified scene. Use `setURL(_:property:value:)` to point a fill at a new asset. ```swift highlight-assetVersioning-updateURI // Point a fill at a new asset URL — for example, after moving assets to a new // CDN or publishing a new version of an image. let migratedURL = URL(string: "https://cdn.example.com/assets/v2/product-photo.jpg")! try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: migratedURL) let updatedURL = try engine.block.getURL(imageFill, property: "fill/image/imageFileURI") print("Updated image URL:", updatedURL.absoluteString) ``` For batch updates, iterate through every graphic block and rewrite the fills that carry an image. A graphic block can hold a color, gradient, image, or video fill, so filter by fill type with `getType(_:)` before touching the URL. ```swift highlight-assetVersioning-findBlocks // To migrate assets in bulk, walk every graphic block and rewrite the fills // that carry an image. Filter by fill type, since a graphic block can hold a // color, gradient, image, or video fill. let graphicBlocks = try engine.block.find(byType: .graphic) for block in graphicBlocks { let fill = try engine.block.getFill(block) guard try engine.block.getType(fill) == FillType.image.rawValue else { continue } let currentURL = try engine.block.getURL(fill, property: "fill/image/imageFileURI") // Rewrite only the assets hosted on the CDN you are retiring. if currentURL.absoluteString.contains("old-cdn.example.com") { let rewritten = currentURL.absoluteString.replacingOccurrences( of: "old-cdn.example.com", with: "new-cdn.example.com", ) if let newURL = URL(string: rewritten) { try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: newURL) } } } ``` This pattern is useful for CDN migrations or restructuring asset directories. ## Strategies for Versioned Asset URLs Designing your URL scheme to support versioning prevents accidental overwrites and makes migrations easier. Three approaches work well. ### Path-Based Versioning Include the version in the URL path: `https://cdn.example.com/assets/v2/logo.png`. When you update assets, increment the version directory. Old designs reference old paths while new designs use new paths, and both versions can coexist on the same CDN. ### Hash-Based Filenames Use content hashes in filenames: `logo-a1b2c3d4.png`. The URL changes whenever the content changes, ensuring automatic cache invalidation. Build tools generate these automatically. This pattern works well for content-addressable storage. ### Query Parameter Versioning Append the version as a query parameter: `logo.png?v=2`. The base URL stays the same but the version parameter forces cache invalidation. Note that some CDNs ignore query parameters for caching — verify your CDN configuration before relying on this approach. ## Best Practices When managing asset URLs in production: - **Use immutable URLs**: Content-addressed or versioned paths prevent accidental overwrites - **Keep old assets available**: Don't delete assets that may be referenced by saved designs - **Use archives for portability**: Export as an archive when designs need to work offline or across environments - **Plan CDN migrations carefully**: Update saved designs before decommissioning old URLs - **Set appropriate cache headers**: Balance performance with freshness requirements - **Document your URL scheme**: Make the versioning strategy clear for your team ## Troubleshooting | Issue | Cause | Solution | |-------|-------|----------| | Asset shows old version | Cached copy of the asset | Use a cache-busting or versioned URL | | Asset not loading | URL changed or deleted | Verify URL accessibility, update the scene | | Design partially loads | Some assets unavailable | Check all asset URLs, consider an archive export | | Archive too large | Many or large embedded assets | Optimize assets before archiving | ## Next Steps - [Save Designs](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) — Save and serialize designs - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Export options including archives --- ## 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: "Integrate Getty Images Stock Photos" description: "Search and import premium stock photography from Getty Images directly into CE.SDK using a secure proxy server." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/getty-images-a3931c/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) > [From Getty Images](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/getty-images-a3931c/) --- ```swift file=@cesdk_swift_examples/engine-guides-import-media-from-remote-source-getty-images/ImportFromGettyImages.swift reference-only import Foundation import IMGLYEngine @MainActor func importFromGettyImages(engine: Engine) async throws { // Replace with the host of your Getty Images proxy server (see the prerequisites). let proxyHost = "YOUR_GETTY_IMAGES_PROXY_HOST" let source = GettyImagesAssetSource(proxyHost: proxyHost) try engine.asset.addSource(source) let registeredSources = engine.asset.findAllSources() print("Registered asset sources: \(registeredSources)") assert(registeredSources.contains(GettyImagesAssetSource.id)) // A live query needs a running proxy server, so skip it while the placeholder host is in place. guard proxyHost != "YOUR_GETTY_IMAGES_PROXY_HOST" else { return } let results = try await engine.asset.findAssets( sourceID: GettyImagesAssetSource.id, query: .init(query: "business", page: 0, perPage: 20), ) print("Getty Images returned \(results.assets.count) photos for 'business'") } ``` ```swift file=@cesdk_swift_examples/third-party/GettyImagesAssetSource.swift reference-only import Foundation import IMGLYEngine public final class GettyImagesAssetSource: NSObject { private let decoder = JSONDecoder() private let host: String private let path: String public init(proxyHost: String, path: String = "/getty-proxy") { host = proxyHost self.path = path } private struct Endpoint { let query: [URLQueryItem] static func search(queryData: AssetQueryData) -> Self { Endpoint( query: [ .init(name: "phrase", value: queryData.query), // Getty Images pages start at 1, while CE.SDK uses 0-based indices. .init(name: "page", value: String(queryData.page + 1)), .init(name: "page_size", value: String(queryData.perPage)), ], ) } func url(host: String, path: String) -> URL? { var components = URLComponents() components.scheme = "https" components.host = host components.path = path components.queryItems = query return components.url } } } extension GettyImagesAssetSource: AssetSource { public static let id = "gettyImagesImageAssets" public var id: String { Self.id } public func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { guard let url = Endpoint.search(queryData: queryData).url(host: host, path: path) else { throw NSError(domain: "GettyImagesAssetSource", code: -1) } let data = try await URLSession.shared.data(from: url).0 let response = try decoder.decode(GettyImagesSearchResponse.self, from: data) let total = response.resultCount ?? -1 let loadedSoFar = (queryData.page + 1) * queryData.perPage let hasNextPage = !response.images.isEmpty && (total < 0 || loadedSoFar < total) return .init( assets: response.images.map(AssetResult.init), currentPage: queryData.page, nextPage: hasNextPage ? queryData.page + 1 : -1, total: total, ) } public var supportedMIMETypes: [String]? { [MIMEType.jpeg.rawValue] } public var credits: AssetCredits? { .init( name: "Getty Images", url: URL(string: "https://www.gettyimages.com/")!, ) } public var license: AssetLicense? { .init( name: "Getty Images Content License Agreement", url: URL(string: "https://www.gettyimages.com/eula")!, ) } } private extension AssetResult { convenience init(image: GettyImage) { // Getty's searchimages display sizes are named (typically "comp", "preview", "thumb"). // Pick the largest for the imported asset and a smaller one for the library thumbnail so // the grid loads lightweight previews instead of full-resolution comps. let sizesByName = Dictionary( image.displaySizes.map { ($0.name, $0.uri) }, uniquingKeysWith: { first, _ in first }, ) let fullURL = sizesByName["comp"] ?? sizesByName["preview"] ?? image.displaySizes.first?.uri let thumbURL = sizesByName["thumb"] ?? sizesByName["preview"] ?? fullURL self.init( id: image.id, locale: "en", label: image.title, meta: [ "uri": fullURL?.absoluteString ?? "", "thumbUri": thumbURL?.absoluteString ?? "", "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "shapeType": ShapeType.rect.rawValue, "kind": "image", "width": image.maxDimensions?.width.map(String.init) ?? "", "height": image.maxDimensions?.height.map(String.init) ?? "", ], context: .init(sourceID: GettyImagesAssetSource.id), ) } } ``` ```swift file=@cesdk_swift_examples/third-party/GettyImagesResponse.swift reference-only import Foundation // MARK: - GettyImagesResponse struct GettyImagesSearchResponse: Decodable { let resultCount: Int? let images: [GettyImage] enum CodingKeys: String, CodingKey { case resultCount = "result_count" case images } } // MARK: - Image struct GettyImage: Decodable { let id: String let title: String? let maxDimensions: GettyImageDimensions? let displaySizes: [GettyDisplaySize] enum CodingKeys: String, CodingKey { case id, title case maxDimensions = "max_dimensions" case displaySizes = "display_sizes" } } // MARK: - Dimensions struct GettyImageDimensions: Decodable { let width: Int? let height: Int? } // MARK: - Display size struct GettyDisplaySize: Decodable { let name: String let uri: URL } ``` Connect CE.SDK to Getty Images' premium stock photography library so users can search and add professionally curated photos to their designs, served through a secure proxy that keeps your API credentials off the device. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-import-media-from-remote-source-getty-images) Getty Images offers premium, professionally curated stock photography through its API. You expose that library inside CE.SDK by implementing a custom asset source: a class that conforms to the `AssetSource` protocol, fetches results from your proxy, and maps them into the asset format the engine understands. Once registered, the source behaves like any other — it can be queried programmatically and surfaced in the asset library. ## Prerequisites - Getty Images API credentials (an API key and secret) from the [Getty Images API Portal](https://developer.gettyimages.com/). - Familiarity with Getty Images' API guidelines, rate limits, and licensing terms. - A secure proxy server that authenticates with Getty Images on the app's behalf. Getty Images requires both a key and a secret, which must never ship inside a client app, so the source talks to your proxy rather than the Getty Images API directly. Setting up the proxy itself is out of scope here; the [web version of this guide](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/getty-images-a3931c/) covers standing one up with the `gettyimages-api` package. This example decodes Getty Images' response and paginates on the client, so its proxy must forward Getty Images' raw `searchimages` JSON unchanged — a thin pass-through, not the response-translating proxy the web guide builds. No third-party dependencies are required on the client — the example uses `URLSession` and `Codable` from Foundation. ## Understanding the Proxy Server Requirement Getty Images authenticates every request with both an API key and a secret. Embedding those credentials in a distributed app would expose them, so a proxy server holds the credentials and forwards requests on the app's behalf: **App (CE.SDK)** → **Your Proxy Server** → **Getty Images API** The app sends a search phrase and pagination to the proxy; the proxy authenticates with Getty Images, runs the search, and returns the results. The example expects the proxy to forward the Getty Images `searchimages` response, so the client decodes Getty Images' native shape and translates it into CE.SDK assets. You configure the source with your proxy's host, and the source builds requests against a `/getty-proxy` path. ## Register the Asset Source A custom asset source is any object conforming to the `AssetSource` protocol. Register it with `engine.asset.addSource(_:)`, passing the host of your proxy so the source can reach it: ```swift highlight-getty-definition // Replace with the host of your Getty Images proxy server (see the prerequisites). let proxyHost = "YOUR_GETTY_IMAGES_PROXY_HOST" let source = GettyImagesAssetSource(proxyHost: proxyHost) try engine.asset.addSource(source) ``` Pass your own proxy host in place of the `YOUR_GETTY_IMAGES_PROXY_HOST` placeholder. The source targets the `/getty-proxy` path on that host; override the `path` argument if your proxy exposes a different endpoint. After registering, the source ID appears in `engine.asset.findAllSources()` alongside the engine's built-in sources: ```swift highlight-getty-verify let registeredSources = engine.asset.findAllSources() print("Registered asset sources: \(registeredSources)") ``` ## Implement findAssets The source wraps a single proxy endpoint that maps to Getty Images' search API. CE.SDK uses 0-based page indices, so it converts each page to the 1-based index Getty Images expects: ```swift highlight-getty-api-creation public final class GettyImagesAssetSource: NSObject { private let decoder = JSONDecoder() private let host: String private let path: String public init(proxyHost: String, path: String = "/getty-proxy") { host = proxyHost self.path = path } private struct Endpoint { let query: [URLQueryItem] static func search(queryData: AssetQueryData) -> Self { Endpoint( query: [ .init(name: "phrase", value: queryData.query), // Getty Images pages start at 1, while CE.SDK uses 0-based indices. .init(name: "page", value: String(queryData.page + 1)), .init(name: "page_size", value: String(queryData.perPage)), ], ) } func url(host: String, path: String) -> URL? { var components = URLComponents() components.scheme = "https" components.host = host components.path = path components.queryItems = query return components.url } } } ``` Conform to `AssetSource` by giving the source a unique `id` and implementing `findAssets(queryData:) async throws -> AssetQueryResult`. The method receives the current query — a search phrase plus pagination — and returns a page of results. Because it is `async`, you can back it with `URLSession`, a cache, or any client: ```swift highlight-getty-find-assets public static let id = "gettyImagesImageAssets" public var id: String { Self.id } public func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { guard let url = Endpoint.search(queryData: queryData).url(host: host, path: path) else { throw NSError(domain: "GettyImagesAssetSource", code: -1) } let data = try await URLSession.shared.data(from: url).0 let response = try decoder.decode(GettyImagesSearchResponse.self, from: data) let total = response.resultCount ?? -1 let loadedSoFar = (queryData.page + 1) * queryData.perPage let hasNextPage = !response.images.isEmpty && (total < 0 || loadedSoFar < total) return .init( assets: response.images.map(AssetResult.init), currentPage: queryData.page, nextPage: hasNextPage ? queryData.page + 1 : -1, total: total, ) } ``` `findAssets` sends the phrase and pagination to the proxy, decodes the Getty Images response, then builds an `AssetQueryResult`: - `currentPage` echoes the requested page. - `nextPage` is the next page to request, or `-1` once a page comes back empty or the loaded results reach the reported total, so the engine stops querying. - `total` is the overall result count Getty Images reports for the query; the source falls back to `-1` when it is absent. ## Translate Getty Images Photos to Assets Each Getty Images photo becomes an `AssetResult`. The `meta` dictionary carries the values the engine needs to add the image to a scene: ```swift highlight-getty-translate convenience init(image: GettyImage) { // Getty's searchimages display sizes are named (typically "comp", "preview", "thumb"). // Pick the largest for the imported asset and a smaller one for the library thumbnail so // the grid loads lightweight previews instead of full-resolution comps. let sizesByName = Dictionary( image.displaySizes.map { ($0.name, $0.uri) }, uniquingKeysWith: { first, _ in first }, ) let fullURL = sizesByName["comp"] ?? sizesByName["preview"] ?? image.displaySizes.first?.uri let thumbURL = sizesByName["thumb"] ?? sizesByName["preview"] ?? fullURL self.init( id: image.id, locale: "en", label: image.title, meta: [ "uri": fullURL?.absoluteString ?? "", "thumbUri": thumbURL?.absoluteString ?? "", "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "shapeType": ShapeType.rect.rawValue, "kind": "image", "width": image.maxDimensions?.width.map(String.init) ?? "", "height": image.maxDimensions?.height.map(String.init) ?? "", ], context: .init(sourceID: GettyImagesAssetSource.id), ) } ``` - `uri` is the full-size `comp` display size; `thumbUri` prefers the smaller `thumb` (or `preview`) size, so the asset library grid loads lightweight previews instead of full-resolution comps. - `blockType` and `fillType` tell the engine to create a graphic block with an image fill when the asset is applied. Setting them up front avoids a round-trip to infer the type from the file. - `shapeType` and `kind` give the block a rectangle shape and mark its content as an image, so it renders correctly without further setup. - `width` and `height` come from the photo's maximum dimensions so the engine can size the block correctly. - `context.sourceID` links the result back to the Getty Images source. ## Handle Licensing and Attribution Getty Images content is premium and is not royalty-free. Describe the provider and its license once at the source level so the editor can surface attribution for every result: ```swift highlight-getty-credits-license public var credits: AssetCredits? { .init( name: "Getty Images", url: URL(string: "https://www.gettyimages.com/")!, ) } public var license: AssetLicense? { .init( name: "Getty Images Content License Agreement", url: URL(string: "https://www.gettyimages.com/eula")!, ) } ``` The license URL points to Getty Images' End User License Agreement for reference. Keep these points in mind: - Getty Images content requires a licensing agreement for production use. - Attribution requirements depend on the license type you hold. - Consult Getty Images' licensing terms for your specific use case. ## Configure the Asset Library UI Registering the source makes it queryable through the Asset API but does not add it to an editor's asset library. On iOS, the [Customize Asset Library](#broken-link-c9a4de) guide covers adding a source to the default library and building a fully custom one. ## Query the Source Once registered, query the source through the standard Asset API. Pass a search phrase and pagination: ```swift highlight-getty-findAssets let results = try await engine.asset.findAssets( sourceID: GettyImagesAssetSource.id, query: .init(query: "business", page: 0, perPage: 20), ) print("Getty Images returned \(results.assets.count) photos for 'business'") ``` The returned `AssetResult` values can be added to a scene with `engine.asset.apply(sourceID:assetResult:)`. ## Troubleshooting - **Authentication errors:** Confirm your proxy authenticates with Getty Images using a valid key and secret, and that the app points at the correct proxy host. - **Rate limiting:** Getty Images enforces per-account request limits. Cache results and avoid re-querying on every keystroke to stay within them. - **Missing attribution:** Verify the source-level `credits` and `license` are populated; the editor uses them to display Getty Images attribution. - **Images fail to load:** Check that the proxy returns usable `display_sizes` URLs and that they are reachable from the device's network over HTTPS. - **Pagination issues:** The client already converts CE.SDK's 0-based index to Getty Images' 1-based page number before sending it; the proxy must forward that page through unchanged rather than converting it again. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addSource(_:)` | Register an object conforming to `AssetSource` (such as `GettyImagesAssetSource`) as a custom source. | | `engine.asset.findAllSources()` | List the IDs of every registered asset source. | | `engine.asset.findAssets(sourceID:query:)` | Query a source for a page of results, returning an `AssetQueryResult`. | | `engine.asset.apply(sourceID:assetResult:)` | Add a queried asset to the scene as a design block. | ## Next Steps - [Customize Asset Library](#broken-link-c9a4de) — Surface the source in the editor's asset panels. - [Integrate Unsplash Images](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/) — Add another remote stock-image source. - [Integrate Pexels Images](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/pexels-90d5df/) — A free royalty-free stock photo alternative. - [Asset Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) — Learn the core import and asset-source concepts. --- ## 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: "Integrate IMG.LY Premium Assets" description: "Host IMG.LY premium templates on your own infrastructure and integrate them into CE.SDK's asset library." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/imgly-premium-assets-eb1688/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) > [From IMG.LY Premium Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/imgly-premium-assets-eb1688/) --- ```swift file=@cesdk_swift_examples/engine-guides-import-media-from-remote-source-imgly-premium-assets/ImglyPremiumAssets.swift reference-only import Foundation import IMGLYEngine // A Decodable model matching the `content.json` manifest that ships with the // IMG.LY premium asset package. Each template carries the design archive `uri` // and a `thumbUri`, both using `{{base_url}}` placeholders so the package stays // portable across hosting locations. private struct PremiumTemplateManifest: Decodable { struct Template: Decodable { let id: String let label: [String: String]? let meta: [String: String] } let id: String let assets: [Template] } @MainActor func imglyPremiumAssets(engine: Engine) async throws { // A design scene the premium templates apply into. In your app this is // whatever the user is currently editing. let scene = try engine.scene.create() 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) // Stand in for the hosted premium package so the example runs offline: save // the current scene to a real design archive under a per-template directory // that plays the role of your hosting location. In production this directory // is your server or CDN and already contains the extracted IMG.LY premium // package (its `content.json`, per-template design archives, and thumbnails). let hostingURL = FileManager.default.temporaryDirectory .appendingPathComponent("premium-\(UUID().uuidString)", isDirectory: true) let templateDirURL = hostingURL.appendingPathComponent("modern-social-story", isDirectory: true) try FileManager.default.createDirectory(at: templateDirURL, withIntermediateDirectories: true) let archive = try await engine.scene.saveToArchive() try archive.write(to: templateDirURL.appendingPathComponent("design.zip")) let baseURL = hostingURL.absoluteString let manifestJSON = """ { "version": "1.0", "id": "imgly-premium-templates", "assets": [ { "id": "modern-social-story", "label": { "en": "Modern Social Media Story" }, "meta": { "uri": "{{base_url}}/modern-social-story/design.zip", "thumbUri": "{{base_url}}/modern-social-story/thumbnail.jpg" } } ] } """ let manifest = try JSONDecoder().decode( PremiumTemplateManifest.self, from: Data(manifestJSON.utf8), ) try engine.asset.addLocalSource(sourceID: manifest.id, 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.load(from: url) return nil }) for template in manifest.assets { let resolvedMeta = template.meta.mapValues { $0.replacingOccurrences(of: "{{base_url}}", with: baseURL) } try engine.asset.addAsset( to: manifest.id, asset: AssetDefinition(id: template.id, meta: resolvedMeta, label: template.label), ) } let templates = try await engine.asset.findAssets( sourceID: manifest.id, query: .init(query: nil, page: 0, perPage: 10), ) print("Premium templates available:", templates.total) if let first = templates.assets.first { let appliedBlock = try await engine.asset.apply(sourceID: manifest.id, assetResult: first) print("Applied template, resulting block:", appliedBlock as Any) } } ``` Host IMG.LY's premium templates on your own infrastructure and integrate them into CE.SDK's asset library alongside your other sources. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-import-media-from-remote-source-imgly-premium-assets) IMG.LY offers premium templates through downloadable asset archives. You host these templates on your own server or CDN and register them as a local asset source, so they appear in the asset library alongside every other source. Because each template is a self-contained design archive, the integration parses a manifest, registers a source with a custom apply callback that loads the archive, and resolves the manifest's hosting placeholders at runtime. Contact IMG.LY sales to purchase premium template packages. ## Prerequisites Before integrating premium assets, ensure you have: - The IMG.LY premium asset archive (contact sales to purchase). - A server or CDN to host the extracted package. - A working CE.SDK `Engine` with a loaded scene. ## Premium Asset Package Structure The archive contains a `content.json` manifest and one directory per template. Each template directory holds three files: `asset.json` with the template's metadata, `thumbnail.jpg` for the asset library preview, and `design.zip` with the complete design and its bundled assets. The manifest lists every template. Each asset's `meta.uri` points at its `design.zip` archive and `meta.thumbUri` at its preview. The `{{base_url}}` placeholder lets you host the package anywhere and resolve the location at runtime. ```json { "version": "1.0", "id": "imgly-premium-templates", "assets": [ { "id": "modern-social-story", "label": { "en": "Modern Social Media Story" }, "meta": { "uri": "{{base_url}}/modern-social-story/design.zip", "thumbUri": "{{base_url}}/modern-social-story/thumbnail.jpg" } } ] } ``` ## Hosting Premium Assets Upload the extracted package to your server or CDN so the `content.json`, each `design.zip`, and each thumbnail are reachable over HTTPS. The `baseURL` you configure points at the directory that holds the package; CE.SDK combines it with the paths from the manifest to fetch each resource. Serving the package over HTTPS keeps it accessible from the device, since Apple platforms block plaintext HTTP by default. ## Fetching and Parsing the Manifest Model the manifest with a `Decodable` type that carries the source `id` and the array of template definitions. ```swift highlight-imglyPremium-model // A Decodable model matching the `content.json` manifest that ships with the // IMG.LY premium asset package. Each template carries the design archive `uri` // and a `thumbUri`, both using `{{base_url}}` placeholders so the package stays // portable across hosting locations. private struct PremiumTemplateManifest: Decodable { struct Template: Decodable { let id: String let label: [String: String]? let meta: [String: String] } let id: String let assets: [Template] } ``` Decode the manifest into that model. It is inline here so the example is self-contained; in production, fetch it from `/content.json` with `URLSession.shared.data(from:)` and decode the response. ```swift highlight-imglyPremium-manifest let manifestJSON = """ { "version": "1.0", "id": "imgly-premium-templates", "assets": [ { "id": "modern-social-story", "label": { "en": "Modern Social Media Story" }, "meta": { "uri": "{{base_url}}/modern-social-story/design.zip", "thumbUri": "{{base_url}}/modern-social-story/thumbnail.jpg" } } ] } """ let manifest = try JSONDecoder().decode( PremiumTemplateManifest.self, from: Data(manifestJSON.utf8), ) ``` The manifest's `id` becomes the asset source identifier you pass to every other Asset API call, and each template's `meta` holds the archive `uri` and the `thumbUri`. ## Creating the Asset Source Register a local source with `addLocalSource(sourceID:applyAsset:)`. Premium templates are `.zip` design archives, so provide a custom `applyAsset` callback that loads each one with `engine.scene.load(from:)` instead of the default apply logic. The callback returns `nil` because loading an archive mutates the current scene rather than creating a new block. ```swift highlight-imglyPremium-source try engine.asset.addLocalSource(sourceID: manifest.id, 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.load(from: url) return nil }) ``` A local source suits a finite, known set of assets: the engine manages search and pagination for you, so you only supply the assets and the apply behavior. The `engine` reference is captured weakly because the source retains this callback for its lifetime. ## Processing and Adding Templates For each template in the manifest, replace the `{{base_url}}` placeholder in every metadata value with your hosting location, then add the processed template to the source with `addAsset(to:asset:)`. ```swift highlight-imglyPremium-addTemplates for template in manifest.assets { let resolvedMeta = template.meta.mapValues { $0.replacingOccurrences(of: "{{base_url}}", with: baseURL) } try engine.asset.addAsset( to: manifest.id, asset: AssetDefinition(id: template.id, meta: resolvedMeta, label: template.label), ) } ``` The engine stores each template and returns it from queries, in insertion order. ## Displaying Templates in the Asset Library Once registered, the source's templates are available to the asset library. On iOS, you surface them in the editor by adding the source to a library category — the [Customize Asset Library](#broken-link-c9a4de) guide walks through presenting a registered source and configuring category labels and panels. ## Applying a Template Query the source to confirm the templates registered, then apply one. Applying runs the source's `applyAsset` callback, which loads the design archive and replaces the current scene — exactly what happens when a user taps a thumbnail in the asset library. ```swift highlight-imglyPremium-verify let templates = try await engine.asset.findAssets( sourceID: manifest.id, query: .init(query: nil, page: 0, perPage: 10), ) print("Premium templates available:", templates.total) ``` ```swift highlight-imglyPremium-apply if let first = templates.assets.first { let appliedBlock = try await engine.asset.apply(sourceID: manifest.id, assetResult: first) print("Applied template, resulting block:", appliedBlock as Any) } ``` ## Optimization Deliver `design.zip` files and thumbnails through a CDN with long cache lifetimes so repeat loads stay fast. For paid content, protect the package behind signed URLs or token-based authentication so only authorized users can fetch the archives. ## Troubleshooting - **Templates not applying** — Premium templates are `.zip` archives. Confirm the source's `applyAsset` callback loads them with `engine.scene.load(from:)` rather than relying on the default apply behavior. - **Assets returning 404** — Verify the package is uploaded with the same directory layout the manifest expects, and that the resolved URLs match your hosting location, including the protocol and any path prefix. - **Media not loading** — Serve the package over HTTPS, or configure an App Transport Security exception; Apple platforms block plaintext HTTP by default. - **Manifest failing to decode** — Confirm the `content.json` structure matches your model, with the `id` and the `assets` array present and each asset carrying a `meta.uri`. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addLocalSource(sourceID:applyAsset:)` | Register a local source with a custom apply callback for loading template archives. | | `engine.asset.addAsset(to:asset:)` | Add a template definition to the local source. | | `engine.scene.load(from:)` | Load a design archive, replacing the current scene. | | `engine.asset.findAssets(sourceID:query:)` | Query a source for a page of templates. | | `engine.asset.apply(sourceID:assetResult:)` | Apply a template, running the source's apply callback. | ## Next Steps - [Integrate Unsplash Stock Images](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/) — Stream royalty-free photos from a custom asset source. - [Customize Asset Library](#broken-link-c9a4de) — On iOS, present your registered sources in the asset library. - [Serve Assets From Your Server](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) — Host CE.SDK's default and sample content on your own infrastructure. - [Asset Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) — Learn how asset sources and metadata fit together. --- ## 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: "Integrate Pexels Stock Images" description: "Search and import high-quality royalty-free stock photos from Pexels directly into CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/pexels-90d5df/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) > [From Pexels](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/pexels-90d5df/) --- ```swift file=@cesdk_swift_examples/engine-guides-import-from-pexels/ImportFromPexels.swift reference-only import Foundation import IMGLYEngine @MainActor func importFromPexels(engine: Engine) async throws { // Replace with your Pexels API key from https://www.pexels.com/api/ let apiKey = "YOUR_PEXELS_API_KEY" let source = PexelsAssetSource(apiKey: apiKey) try engine.asset.addSource(source) let registeredSources = engine.asset.findAllSources() print("Registered asset sources: \(registeredSources)") assert(registeredSources.contains(PexelsAssetSource.id)) // A live query needs a real API key, so skip it while the placeholder is in place. guard apiKey != "YOUR_PEXELS_API_KEY" else { return } let curated = try await engine.asset.findAssets( sourceID: PexelsAssetSource.id, query: .init(query: nil, page: 0, perPage: 20), ) print("Pexels returned \(curated.assets.count) curated photos") let results = try await engine.asset.findAssets( sourceID: PexelsAssetSource.id, query: .init(query: "mountains", page: 0, perPage: 20), ) print("Pexels search returned \(results.assets.count) photos for 'mountains'") } ``` ```swift file=@cesdk_swift_examples/third-party/PexelsAssetSource.swift reference-only import Foundation import IMGLYEngine public final class PexelsAssetSource: NSObject { private let decoder = JSONDecoder() private let apiKey: String public init(apiKey: String) { self.apiKey = apiKey } private struct Endpoint { let path: String let query: [URLQueryItem] static func search(queryData: AssetQueryData) -> Self { Endpoint( path: "/v1/search", query: [ .init(name: "query", value: queryData.query), .init(name: "page", value: String(queryData.page + 1)), .init(name: "per_page", value: String(queryData.perPage)), ], ) } static func curated(queryData: AssetQueryData) -> Self { Endpoint( path: "/v1/curated", query: [ .init(name: "page", value: String(queryData.page + 1)), .init(name: "per_page", value: String(queryData.perPage)), ], ) } var url: URL? { var components = URLComponents() components.scheme = "https" components.host = "api.pexels.com" components.path = path components.queryItems = query return components.url } } } extension PexelsAssetSource: AssetSource { public static let id = "pexels" public var id: String { Self.id } public func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { let endpoint: Endpoint = queryData.query? .isEmpty ?? true ? .curated(queryData: queryData) : .search(queryData: queryData) var request = URLRequest(url: endpoint.url!) request.setValue(apiKey, forHTTPHeaderField: "Authorization") let data = try await URLSession.shared.data(for: request).0 let response = try decoder.decode(PexelsSearchResponse.self, from: data) let hasNextPage = response.nextPage != nil && !response.photos.isEmpty return .init( assets: response.photos.map(AssetResult.init), currentPage: queryData.page, nextPage: hasNextPage ? queryData.page + 1 : -1, total: response.totalResults ?? -1, ) } public var supportedMIMETypes: [String]? { [MIMEType.jpeg.rawValue] } public var credits: AssetCredits? { .init( name: "Pexels", url: URL(string: "https://www.pexels.com/")!, ) } public var license: AssetLicense? { .init( name: "Pexels license (free)", url: URL(string: "https://www.pexels.com/license/")!, ) } } private extension AssetResult { convenience init(photo: PexelsPhoto) { self.init( id: String(photo.id), locale: "en", label: photo.alt, meta: [ "uri": photo.src.original.absoluteString, "thumbUri": photo.src.medium.absoluteString, "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "shapeType": ShapeType.rect.rawValue, "kind": "image", "width": String(photo.width), "height": String(photo.height), ], context: .init(sourceID: PexelsAssetSource.id), credits: .init( name: photo.photographer, url: photo.photographerURL.flatMap(URL.init(string:)), ), utm: .init(source: "CE.SDK Demo", medium: "referral"), ) } } ``` ```swift file=@cesdk_swift_examples/third-party/PexelsResponse.swift reference-only import Foundation // MARK: - PexelsResponse struct PexelsSearchResponse: Decodable { let totalResults: Int? let nextPage: String? let photos: [PexelsPhoto] enum CodingKeys: String, CodingKey { case totalResults = "total_results" case nextPage = "next_page" case photos } } // MARK: - Photo struct PexelsPhoto: Decodable { let id: Int let width: Int let height: Int let photographer: String let photographerURL: String? let alt: String? let src: PexelsPhotoSource enum CodingKeys: String, CodingKey { case id, width, height, photographer, alt, src case photographerURL = "photographer_url" } } // MARK: - Source struct PexelsPhotoSource: Decodable { let original: URL let medium: URL } ``` Connect CE.SDK to the Pexels API to search and add royalty-free stock photos directly to your designs. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-import-from-pexels) Pexels offers a large library of high-quality, royalty-free stock photos through a public REST API. You expose that library inside CE.SDK by implementing a custom asset source: a small class that conforms to the `AssetSource` protocol, fetches results from the Pexels API, and maps them into the asset format the engine understands. Once registered, the source behaves like any other — it can be queried programmatically and surfaced in the asset library. ## Prerequisites - A Pexels API key from the [Pexels API documentation](https://www.pexels.com/api/documentation/). - Familiarity with Pexels' [API guidelines](https://www.pexels.com/api/documentation/#guidelines) and rate limits. No third-party dependencies are required — the example uses `URLSession` and `Codable` from Foundation. ## Register the Asset Source A custom asset source is any object conforming to the `AssetSource` protocol. Register it with `engine.asset.addSource(_:)`, passing your Pexels API key so the source can authenticate its requests: ```swift highlight-pexels-definition // Replace with your Pexels API key from https://www.pexels.com/api/ let apiKey = "YOUR_PEXELS_API_KEY" let source = PexelsAssetSource(apiKey: apiKey) try engine.asset.addSource(source) ``` Pass your own key in place of the `YOUR_PEXELS_API_KEY` placeholder. Pexels expects the key in the `Authorization` header of every request, which the source sets for you. After registering, the source ID appears in `engine.asset.findAllSources()` alongside the engine's built-in sources: ```swift highlight-pexels-verify let registeredSources = engine.asset.findAllSources() print("Registered asset sources: \(registeredSources)") ``` ## Implement findAssets The source wraps two Pexels endpoints — `/v1/search` for queries and `/v1/curated` for browsing without a search term. CE.SDK uses 0-based page indices, so it converts each page to the 1-based index Pexels expects: ```swift highlight-pexels-api-creation public final class PexelsAssetSource: NSObject { private let decoder = JSONDecoder() private let apiKey: String public init(apiKey: String) { self.apiKey = apiKey } private struct Endpoint { let path: String let query: [URLQueryItem] static func search(queryData: AssetQueryData) -> Self { Endpoint( path: "/v1/search", query: [ .init(name: "query", value: queryData.query), .init(name: "page", value: String(queryData.page + 1)), .init(name: "per_page", value: String(queryData.perPage)), ], ) } static func curated(queryData: AssetQueryData) -> Self { Endpoint( path: "/v1/curated", query: [ .init(name: "page", value: String(queryData.page + 1)), .init(name: "per_page", value: String(queryData.perPage)), ], ) } var url: URL? { var components = URLComponents() components.scheme = "https" components.host = "api.pexels.com" components.path = path components.queryItems = query return components.url } } } ``` Conform to `AssetSource` by giving the source a unique `id` and implementing `findAssets(queryData:) async throws -> AssetQueryResult`. The method receives the current query — a search string plus pagination — and returns a page of results. Because it is `async`, you can back it with `URLSession`, a cache, or any third-party client: ```swift highlight-pexels-find-assets public static let id = "pexels" public var id: String { Self.id } public func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { let endpoint: Endpoint = queryData.query? .isEmpty ?? true ? .curated(queryData: queryData) : .search(queryData: queryData) var request = URLRequest(url: endpoint.url!) request.setValue(apiKey, forHTTPHeaderField: "Authorization") let data = try await URLSession.shared.data(for: request).0 let response = try decoder.decode(PexelsSearchResponse.self, from: data) let hasNextPage = response.nextPage != nil && !response.photos.isEmpty return .init( assets: response.photos.map(AssetResult.init), currentPage: queryData.page, nextPage: hasNextPage ? queryData.page + 1 : -1, total: response.totalResults ?? -1, ) } ``` `findAssets` routes to `/v1/search` when the query string is non-empty and to `/v1/curated` otherwise, sends the request with the API key in the `Authorization` header, then decodes the response into an `AssetQueryResult`: - `currentPage` echoes the requested page. - `nextPage` is the next page to request, or `-1` when no more results exist — Pexels returns a `next_page` URL only while there are more. - `total` is the overall result count; the curated endpoint omits it, so the source reports `-1` to signal an unknown total. ## Translate Pexels Photos to Assets Each Pexels photo becomes an `AssetResult`. The `meta` dictionary carries the values the engine needs to add the image to a scene: ```swift highlight-pexels-translate convenience init(photo: PexelsPhoto) { self.init( id: String(photo.id), locale: "en", label: photo.alt, meta: [ "uri": photo.src.original.absoluteString, "thumbUri": photo.src.medium.absoluteString, "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "shapeType": ShapeType.rect.rawValue, "kind": "image", "width": String(photo.width), "height": String(photo.height), ], context: .init(sourceID: PexelsAssetSource.id), credits: .init( name: photo.photographer, url: photo.photographerURL.flatMap(URL.init(string:)), ), utm: .init(source: "CE.SDK Demo", medium: "referral"), ) } ``` - `uri` is the full-resolution image (`src.original`); `thumbUri` is the preview shown in the asset library (`src.medium`). - `blockType` and `fillType` tell the engine to create a graphic block with an image fill when the asset is applied. Setting them up front avoids a round-trip to infer the type from the file. - `shapeType` and `kind` give the block a rectangle shape and mark its content as an image, so it renders correctly without further setup. - `width` and `height` come straight from the API so the engine can size the block correctly. - `context.sourceID` links the result back to the Pexels source. ## Handle Attribution Pexels' license asks you to credit photographers. Provide attribution at two levels. Source-level `credits` and `license` describe the provider and apply to every result: ```swift highlight-pexels-credits-license public var credits: AssetCredits? { .init( name: "Pexels", url: URL(string: "https://www.pexels.com/")!, ) } public var license: AssetLicense? { .init( name: "Pexels license (free)", url: URL(string: "https://www.pexels.com/license/")!, ) } ``` Per-asset `credits` (set in the translation above) name the individual photographer with a link to their Pexels profile, and the `utm` parameters tag outgoing links for analytics. ## Query the Source Once registered, query the source through the standard Asset API. Pass an empty query to fetch curated photos, or a search term to search the library — pagination works the same way for both: ```swift highlight-pexels-findAssets let curated = try await engine.asset.findAssets( sourceID: PexelsAssetSource.id, query: .init(query: nil, page: 0, perPage: 20), ) print("Pexels returned \(curated.assets.count) curated photos") let results = try await engine.asset.findAssets( sourceID: PexelsAssetSource.id, query: .init(query: "mountains", page: 0, perPage: 20), ) print("Pexels search returned \(results.assets.count) photos for 'mountains'") ``` The returned `AssetResult` values can be added to a scene with `engine.asset.apply(sourceID:assetResult:)`. Registering the source makes it queryable through the Asset API but does not add it to an editor's asset library — on iOS, add a library section for it as shown in the [Customize Asset Library](#broken-link-c9a4de) guide. ## Troubleshooting - **Authentication errors:** Confirm the key is sent in the `Authorization` header and is valid for the Pexels API. - **Rate limiting:** Pexels enforces per-key rate limits and returns HTTP 429 when exceeded. Cache results and back off on 429 responses. - **Missing attribution:** Verify both the source-level `credits`/`license` and the per-asset `credits` are populated from the API response. - **Images fail to load:** Check that the Pexels CDN URLs in `src` are reachable from the device's network. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addSource(_:)` | Register an object conforming to `AssetSource` (such as `PexelsAssetSource`) as a custom source. | | `engine.asset.findAllSources()` | List the IDs of every registered asset source. | | `engine.asset.findAssets(sourceID:query:)` | Query a source for a page of results, returning an `AssetQueryResult`. | ## Next Steps - [Integrate Unsplash Images](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/) — Add another remote stock-image source. - [Customize Asset Library](#broken-link-c9a4de) — On iOS, surface the source in the editor's asset panels. - [Insert Shapes or Stickers](https://img.ly/docs/cesdk/mac-catalyst/insert-media/shapes-or-stickers-20ac68/) — Query and apply assets from a registered source. - [Serve Assets From Your Server](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) — Host and register your own asset content. - [Asset Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) — Learn the core import and asset-source concepts. --- ## 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 Remote Assets" description: "Load asset definitions from remote JSON files hosted on CDNs or servers into CE.SDK's asset library with filtering and custom base URLs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/remote-asset-484685/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) > [Import Remote Asset](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/remote-asset-484685/) --- ```swift file=@cesdk_swift_examples/engine-guides-remote-asset/RemoteAsset.swift reference-only import Foundation import IMGLYEngine @MainActor func remoteAsset(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) // Base URL where the asset files are hosted. In production this is your CDN or // server; substitute your own base URL here. let baseURL = try engine.guidesBaseURL let imageSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.image/content.json"), ) print("Loaded source:", imageSourceID) let absoluteImageURL = baseURL .appendingPathComponent("ly.img.image/images/sample_1.jpg") .absoluteString let manifestJSON = """ { "version": "2.0.0", "id": "my.remote.images", "assets": [ { "id": "sample_image", "label": { "en": "Sample Image" }, "meta": { "uri": "\(absoluteImageURL)", "thumbUri": "\(absoluteImageURL)", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "mimeType": "image/jpeg" } } ] } """ let stringSourceID = try engine.asset.addLocalAssetSourceFromJSON(manifestJSON) print("Loaded source:", stringSourceID) let hostedManifest = """ { "version": "2.0.0", "id": "my.remote.images.hosted", "assets": [ { "id": "sample_image", "label": { "en": "Sample Image" }, "meta": { "uri": "{{base_url}}/ly.img.image/images/sample_1.jpg", "thumbUri": "{{base_url}}/ly.img.image/images/sample_1.jpg", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "mimeType": "image/jpeg" } } ] } """ let hostedSourceID = try engine.asset.addLocalAssetSourceFromJSON( hostedManifest, basePath: baseURL.absoluteString, ) print("Loaded source:", hostedSourceID) let results = try await engine.asset.findAssets( sourceID: "my.remote.images", query: .init(query: nil, page: 0, perPage: 10), ) print("Found assets:", results.total) let hostedAssets = try await engine.asset.findAssets( sourceID: "my.remote.images.hosted", query: .init(query: nil, page: 0, perPage: 10), ) if let asset = hostedAssets.assets.first { let blockID = try await engine.asset.apply(sourceID: "my.remote.images.hosted", assetResult: asset) print("Applied asset to block:", blockID as Any) } let sources = engine.asset.findAllSources() print("Registered sources:", sources) try engine.asset.removeSource(sourceID: "my.remote.images") do { let sourceID = try engine.asset.addLocalAssetSourceFromJSON("{ not valid json }") print("Loaded source:", sourceID) } catch { print("Failed to load asset source:", error.localizedDescription) } } // Compile-only variant showing the URL overload with a fully-qualified remote // URL. The test above runs `remoteAsset` against the bundled sample assets; this // function is here to illustrate how the same call looks against a CDN. @MainActor func remoteAssetFromRemoteServer(engine: Engine) async throws { let baseURL = URL(string: "https://cdn.example.com/assets")! let sourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("my-source/content.json"), ) print("Loaded source:", sourceID) } ``` Load asset definitions from remote JSON files hosted on a CDN or server into CE.SDK's asset library. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-remote-asset) Remote asset loading lets you host asset definitions on a CDN or server and load them into CE.SDK at runtime. This keeps asset management separate from your app, so you can update the available assets without shipping a new build. `engine.asset.addLocalAssetSourceFromJSON(_:)` loads a manifest from a URL, and the string overload loads one you already have in memory. ## Setup Create a scene and a page so there is something to apply assets to. ```swift highlight-remoteAsset-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) ``` ## JSON Manifest Structure A manifest declares a `version`, a source `id`, and an `assets` array. Each asset carries an `id`, localized labels, and a `meta` object with the URIs and block type. ```json { "version": "2.0.0", "id": "my.remote.images", "assets": [ { "id": "sample_image", "label": { "en": "Sample Image" }, "meta": { "uri": "{{base_url}}/images/sample.jpg", "thumbUri": "{{base_url}}/thumbnails/sample.jpg", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "mimeType": "image/jpeg" } } ] } ``` The `id` becomes the asset source identifier you pass to every other Asset API call. Each asset's `meta` holds the full-size `uri`, the `thumbUri`, the `blockType` to create when applied, the `fillType` to attach, and the `mimeType`. The `{{base_url}}` placeholder resolves against the manifest's location or a base path you provide. ## Loading Assets from a Remote URL Pass the manifest's URL to the async `addLocalAssetSourceFromJSON(_:)` overload. It fetches and parses the file and returns the source ID from the manifest's `id` field. CE.SDK resolves `{{base_url}}` placeholders against the manifest's parent directory. Here `baseURL` is the location where your asset files are hosted. ```swift highlight-remoteAsset-loadFromURL let imageSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.image/content.json"), ) print("Loaded source:", imageSourceID) ``` In production you pass a fully-qualified URL pointing at your CDN or server: ```swift highlight-remoteAsset-remoteServer let baseURL = URL(string: "https://cdn.example.com/assets")! let sourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("my-source/content.json"), ) print("Loaded source:", sourceID) ``` ## Loading Assets from a JSON String When you already have the manifest content — for example from an API response or a configuration value — use the synchronous string overload. When the manifest contains absolute URLs, no base path is needed. ```swift highlight-remoteAsset-loadFromString let absoluteImageURL = baseURL .appendingPathComponent("ly.img.image/images/sample_1.jpg") .absoluteString let manifestJSON = """ { "version": "2.0.0", "id": "my.remote.images", "assets": [ { "id": "sample_image", "label": { "en": "Sample Image" }, "meta": { "uri": "\(absoluteImageURL)", "thumbUri": "\(absoluteImageURL)", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "mimeType": "image/jpeg" } } ] } """ let stringSourceID = try engine.asset.addLocalAssetSourceFromJSON(manifestJSON) print("Loaded source:", stringSourceID) ``` ## Customizing the Base Path When the manifest uses `{{base_url}}` placeholders for relative paths, pass a `basePath` so CE.SDK can resolve them against your hosting location. ```swift highlight-remoteAsset-basePath let hostedManifest = """ { "version": "2.0.0", "id": "my.remote.images.hosted", "assets": [ { "id": "sample_image", "label": { "en": "Sample Image" }, "meta": { "uri": "{{base_url}}/ly.img.image/images/sample_1.jpg", "thumbUri": "{{base_url}}/ly.img.image/images/sample_1.jpg", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "mimeType": "image/jpeg" } } ] } """ let hostedSourceID = try engine.asset.addLocalAssetSourceFromJSON( hostedManifest, basePath: baseURL.absoluteString, ) print("Loaded source:", hostedSourceID) ``` ## Verifying Loaded Assets Call `findAssets(sourceID:query:)` to query a loaded source. This confirms the manifest was parsed and the assets are available. ```swift highlight-remoteAsset-verify let results = try await engine.asset.findAssets( sourceID: "my.remote.images", query: .init(query: nil, page: 0, perPage: 10), ) print("Found assets:", results.total) ``` ## Applying Remote Assets Use `apply(sourceID:assetResult:)` to add an asset from a loaded source to the scene. The engine downloads the underlying media when it is needed and returns the ID of the created block. ```swift highlight-remoteAsset-apply let hostedAssets = try await engine.asset.findAssets( sourceID: "my.remote.images.hosted", query: .init(query: nil, page: 0, perPage: 10), ) if let asset = hostedAssets.assets.first { let blockID = try await engine.asset.apply(sourceID: "my.remote.images.hosted", assetResult: asset) print("Applied asset to block:", blockID as Any) } ``` ## Listing Asset Sources Call `findAllSources()` to list the IDs of every registered asset source. ```swift highlight-remoteAsset-listSources let sources = engine.asset.findAllSources() print("Registered sources:", sources) ``` ## Removing Asset Sources Call `removeSource(sourceID:)` to remove a source you no longer need, freeing its assets from memory. ```swift highlight-remoteAsset-removeSource try engine.asset.removeSource(sourceID: "my.remote.images") ``` ## Error Handling These methods throw on failure. The URL overload can fail on network errors or a missing file; both overloads fail on malformed JSON. Wrap calls in a `do`/`catch` block to handle these cases. ```swift highlight-remoteAsset-errorHandling do { let sourceID = try engine.asset.addLocalAssetSourceFromJSON("{ not valid json }") print("Loaded source:", sourceID) } catch { print("Failed to load asset source:", error.localizedDescription) } ``` ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addLocalAssetSourceFromJSON(_:)` | Load asset definitions from a JSON file URL. Returns the source ID. | | `engine.asset.addLocalAssetSourceFromJSON(_:basePath:)` | Load asset definitions from a JSON string, resolving `{{base_url}}` placeholders against `basePath`. Returns the source ID. | | `engine.asset.findAssets(sourceID:query:)` | Query the assets in a loaded source. | | `engine.asset.apply(sourceID:assetResult:)` | Apply an asset to the scene, creating a block. | | `engine.asset.findAllSources()` | List the IDs of all registered asset sources. | | `engine.asset.removeSource(sourceID:)` | Remove a loaded asset source. | ## Next Steps - [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) — How asset sources and assets fit together. - [Serve Assets From Your Server](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) — Host the asset files behind a manifest on your own server or CDN. - [Integrate Unsplash Stock Images](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/) — See a complete custom `AssetSource` implementation backed by a remote API. --- ## 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: "Integrate Third-Party APIs" description: "Import media from any third-party service into CE.SDK using custom asset sources." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/third-party-a05854/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) > [From Third-Party](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/third-party-a05854/) --- Connect any third-party API to CE.SDK through the custom asset source mechanism and import media directly into your creative app. ## Custom Asset Sources CE.SDK's asset source system provides a flexible way to connect to any third-party API. Whether you're working with stock images, audio libraries, or custom data sources, a custom asset source handles search, pagination, and asset management for you. It works with any REST API, giving you full control over how media is fetched. ## Available Integration Examples A complete, runnable integration is available for Unsplash: - [Unsplash](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/) — Browse and import royalty-free photos, with search, pagination, and credits handled through a custom asset source. The same approach extends to any provider — other stock-photo libraries, royalty-free audio or video services, or your own backend. Each integration differs only in the REST endpoints it calls and how it maps the responses into assets. ## Common Integration Patterns Most third-party integrations share the same building blocks: - **Search** — Let users search the third-party library by query string. - **Pagination** — Handle large result sets with page-based loading. - **Asset preview** — Provide thumbnails and metadata so a preview is available before the full asset is imported. - **Authentication** — Keep API keys secure by routing requests through a proxy server. ## Next Steps - [Asset Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) — Understand how asset sources organize content and connect to the rest of the asset system. - [From Pexels](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/pexels-90d5df/) — Connect CE.SDK to Pexels API to search, browse, and add royalty-free stock photos directly to designs. - [Integrate Getty Images Stock Photos](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/getty-images-a3931c/) — Search and import premium stock photography from Getty Images directly into CE.SDK using a secure proxy server. - [Source Sets](https://img.ly/docs/cesdk/mac-catalyst/import-media/source-sets-5679c8/) — Serve multiple resolutions of an asset for performance 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: "Integrate Unsplash Stock Images" description: "Search and import high-quality stock images from Unsplash directly into CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) > [From Unsplash](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/) --- ```swift file=@cesdk_swift_examples/engine-guides-custom-asset-source/CustomAssetSource.swift reference-only import Foundation import IMGLYEngine @MainActor func customAssetSource(engine: Engine) async throws { let source = UnsplashAssetSource(host: secrets.unsplashHost) try engine.asset.addSource(source) let list = try await engine.asset.findAssets( sourceID: "ly.img.asset.source.unsplash", query: .init(query: "", page: 1, perPage: 10), ) let search = try await engine.asset.findAssets( sourceID: "ly.img.asset.source.unsplash", query: .init(query: "banana", page: 1, perPage: 10), ) try engine.asset.addLocalSource(sourceID: "background-videos") let asset = AssetDefinition(id: "ocean-waves-1", meta: [ "uri": "https://example.com/ocean-waves-1.mp4", "thumbUri": "https://example.com/thumbnails/ocean-waves-1.jpg", "mimeType": "video/mp4", "width": "1920", "height": "1080", ], label: [ "en": "relaxing ocean waves", "es": "olas del mar relajantes", ], tags: [ "en": ["ocean", "waves", "soothing", "slow"], "es": ["mar", "olas", "calmante", "lento"], ]) try engine.asset.addAsset(to: "background-videos", asset: asset) } ``` ```swift file=@cesdk_swift_examples/third-party/UnsplashAssetSource.swift reference-only import Foundation import IMGLYEngine public final class UnsplashAssetSource: NSObject { private lazy var decoder: JSONDecoder = { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase return decoder }() private let host: String private let path: String public init(host: String, path: String = "/unsplashProxy") { self.host = host self.path = path } private struct Endpoint { let path: String let query: [URLQueryItem] static func search(queryData: AssetQueryData) -> Self { Endpoint( path: "/search/photos", query: [ .init(name: "query", value: queryData.query), .init(name: "page", value: String(queryData.page + 1)), .init(name: "per_page", value: String(queryData.perPage)), .init(name: "content_filter", value: "high"), ], ) } static func list(queryData: AssetQueryData) -> Self { Endpoint( path: "/photos", query: [ .init(name: "order_by", value: "popular"), .init(name: "page", value: String(queryData.page + 1)), .init(name: "per_page", value: String(queryData.perPage)), .init(name: "content_filter", value: "high"), ], ) } func url(with host: String, path: String) -> URL? { var components = URLComponents() components.scheme = "https" components.host = host components.path = path + self.path components.queryItems = query return components.url } } } extension UnsplashAssetSource: AssetSource { public static let id = "ly.img.asset.source.unsplash" public var id: String { Self.id } public func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { let endpoint: Endpoint = queryData.query? .isEmpty ?? true ? .list(queryData: queryData) : .search(queryData: queryData) let data = try await URLSession.shared.data(from: endpoint.url(with: host, path: path)!).0 if queryData.query?.isEmpty ?? true { let response = try decoder.decode(UnsplashListResponse.self, from: data) let nextPage = queryData.page + 1 return .init( assets: response.map(AssetResult.init), currentPage: queryData.page, nextPage: nextPage, total: -1, ) } else { let response = try decoder.decode(UnsplashSearchResponse.self, from: data) let (results, total, totalPages) = (response.results, response.total ?? 0, response.totalPages ?? 0) let nextPage = (queryData.page + 1) == totalPages ? -1 : queryData.page + 1 return .init( assets: results.map(AssetResult.init), currentPage: queryData.page, nextPage: nextPage, total: total, ) } } public var supportedMIMETypes: [String]? { [MIMEType.jpeg.rawValue] } public var credits: AssetCredits? { .init( name: "Unsplash", url: URL(string: "https://unsplash.com/")!, ) } public var license: AssetLicense? { .init( name: "Unsplash license (free)", url: URL(string: "https://unsplash.com/license")!, ) } } private extension AssetResult { convenience init(image: UnsplashImage) { self.init( id: image.id, locale: "en", label: image.description ?? image.altDescription, tags: image.tags?.compactMap(\.title), meta: [ "uri": image.urls.full.absoluteString, "thumbUri": image.urls.thumb.absoluteString, "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "shapeType": ShapeType.rect.rawValue, "kind": "image", "width": String(image.width), "height": String(image.height), "looping": "false", ], context: .init(sourceID: "unsplash"), credits: .init(name: image.user.name!, url: image.user.links?.html), utm: .init(source: "CE.SDK Demo", medium: "referral"), ) } } ``` ```swift file=@cesdk_swift_examples/third-party/UnsplashResponse.swift reference-only import Foundation // MARK: - UnsplashResponse struct UnsplashSearchResponse: Decodable { let total, totalPages: Int? let results: [UnsplashImage] } typealias UnsplashListResponse = [UnsplashImage] // MARK: - Result struct UnsplashImage: Decodable { let id: String let createdAt, updatedAt: String let promotedAt: String? let width, height: Int let color, blurHash: String? let description: String? let altDescription: String? let urls: Urls let likes: Int? let likedByUser: Bool? let user: User let tags: [Tag]? } // MARK: - Tag struct Tag: Decodable { let type, title: String? } // MARK: - Urls struct Urls: Decodable { let raw, full, regular, small: URL let thumb, smallS3: URL } // MARK: - User struct User: Decodable { let id: String let updatedAt: String let username, name, firstName: String? let lastName, twitterUsername: String? let portfolioURL: String? let bio, location: String? let links: UserLinks? let instagramUsername: String? let totalCollections, totalLikes, totalPhotos: Int? let acceptedTos, forHire: Bool? } // MARK: - UserLinks struct UserLinks: Decodable { let linksSelf, html, photos, likes: URL? let portfolio, following, followers: URL? } ``` Browse Unsplash's library of royalty-free photos from inside the editor by registering a custom asset source. The engine calls your `findAssets` implementation as the user searches and scrolls, so results stream in from the Unsplash API on demand. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-custom-asset-source) CE.SDK lets you plug external image providers — like Unsplash or your own backend — into the engine as custom asset sources. This guide builds an Unsplash source, maps its REST responses to the engine's asset format, handles attribution, surfaces the source in the asset library, and shows the engine-managed local-source alternative. ## Prerequisites - An Unsplash API access key from the [Unsplash Developer portal](https://unsplash.com/developers). - A proxy that forwards requests to the Unsplash API. The example reads the proxy host from `secrets.unsplashHost` — a secrets shim the [iOS guides repository](https://github.com/imgly/cesdk-swift-examples/blob/v$UBQ_VERSION$/secrets/Secrets.swift) ships, which you replace with your own host string. Unsplash's guidelines ask you to proxy requests rather than embed your access key in the app; setting up the proxy itself is out of scope here. ## Setting Up the Unsplash API Client The example wraps the Unsplash REST API in a class. The setup holds two endpoint definitions — `/search/photos` for queries and `/photos` for popular images — along with a JSON decoder configured to convert Unsplash's snake-case keys. ```swift highlight-unsplash-api-creation public final class UnsplashAssetSource: NSObject { private lazy var decoder: JSONDecoder = { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase return decoder }() private let host: String private let path: String public init(host: String, path: String = "/unsplashProxy") { self.host = host self.path = path } private struct Endpoint { let path: String let query: [URLQueryItem] static func search(queryData: AssetQueryData) -> Self { Endpoint( path: "/search/photos", query: [ .init(name: "query", value: queryData.query), .init(name: "page", value: String(queryData.page + 1)), .init(name: "per_page", value: String(queryData.perPage)), .init(name: "content_filter", value: "high"), ], ) } static func list(queryData: AssetQueryData) -> Self { Endpoint( path: "/photos", query: [ .init(name: "order_by", value: "popular"), .init(name: "page", value: String(queryData.page + 1)), .init(name: "per_page", value: String(queryData.perPage)), .init(name: "content_filter", value: "high"), ], ) } func url(with host: String, path: String) -> URL? { var components = URLComponents() components.scheme = "https" components.host = host components.path = path + self.path components.queryItems = query return components.url } } } ``` ## Creating the Unsplash Asset Source Definition Register an asset source by passing an object that implements the `AssetSource` protocol to `addSource(_:)`. Each source needs a unique identifier — every Asset API call references it. ```swift highlight-unsplash-definition let source = UnsplashAssetSource(host: secrets.unsplashHost) try engine.asset.addSource(source) ``` The single method every source must implement is `findAssets(queryData:)`. It receives the query the engine wants — a search string plus pagination — and returns the matching slice of assets along with the current page, the next page, and the total number available. Because it is `async`, you are free to back it with `URLSession`, local storage, a cache, or a third-party SDK. ## Implementing Search and Discovery Unsplash uses different endpoints for searching versus browsing. Inspect the query string to decide which one to call: an empty query lists popular photos from `/photos`, while a non-empty query hits `/search/photos`. The `queryData` carries everything you need: - `queryData.query` — the current search term from the asset library's search bar. - `queryData.page` — the zero-based page index the engine requests; pages load as the user scrolls. The example adds `1` when calling Unsplash because Unsplash's pages start at `1`. - `queryData.perPage` — how many assets to return per page; this can change between calls (a small number for a preview, a larger one for a grid). ```swift highlight-unsplash-query let endpoint: Endpoint = queryData.query? .isEmpty ?? true ? .list(queryData: queryData) : .search(queryData: queryData) ``` Once the response arrives, map it to the `AssetQueryResult` the engine expects: - `assets` — the assets for this page. - `total` — the total number available for the query. The popular-images branch can't know this ahead of time, so it returns `-1`. - `currentPage` — the page that was requested. - `nextPage` — the next page to request, or `-1` when there are no more results so the engine stops querying. ```swift highlight-unsplash-result-mapping if queryData.query?.isEmpty ?? true { let response = try decoder.decode(UnsplashListResponse.self, from: data) let nextPage = queryData.page + 1 return .init( assets: response.map(AssetResult.init), currentPage: queryData.page, nextPage: nextPage, total: -1, ) } else { let response = try decoder.decode(UnsplashSearchResponse.self, from: data) let (results, total, totalPages) = (response.results, response.total ?? 0, response.totalPages ?? 0) let nextPage = (queryData.page + 1) == totalPages ? -1 : queryData.page + 1 return .init( assets: results.map(AssetResult.init), currentPage: queryData.page, nextPage: nextPage, total: total, ) } ``` ## Translating Unsplash Data to CE.SDK Format Each Unsplash photo is translated into an `AssetResult`. The `id` is mandatory and must be unique within the source; every other field is optional but improves the experience. ```swift highlight-translateToAssetResult convenience init(image: UnsplashImage) { self.init( id: image.id, locale: "en", label: image.description ?? image.altDescription, tags: image.tags?.compactMap(\.title), meta: [ "uri": image.urls.full.absoluteString, "thumbUri": image.urls.thumb.absoluteString, "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "shapeType": ShapeType.rect.rawValue, "kind": "image", "width": String(image.width), "height": String(image.height), "looping": "false", ], context: .init(sourceID: "unsplash"), credits: .init(name: image.user.name!, url: image.user.links?.html), utm: .init(source: "CE.SDK Demo", medium: "referral"), ) } ``` `id` — the asset's unique identifier. ```swift highlight-result-id id: image.id, ``` `locale` — the language locale used for `label` and `tags`. ```swift highlight-result-locale locale: "en", ``` `label` — a human-readable name, shown in tooltips and credits. ```swift highlight-result-label label: image.description ?? image.altDescription, ``` `tags` — searchable keywords, also shown in credits. ```swift highlight-result-tags tags: image.tags?.compactMap(\.title), ``` The `meta` dictionary holds the type-specific properties that tell the engine how to apply the asset. ```swift highlight-result-meta meta: [ "uri": image.urls.full.absoluteString, "thumbUri": image.urls.thumb.absoluteString, "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "shapeType": ShapeType.rect.rawValue, "kind": "image", "width": String(image.width), "height": String(image.height), "looping": "false", ], ``` `uri` — the URL of the full-resolution image used when the asset is added to the scene. ```swift highlight-result-uri "uri": image.urls.full.absoluteString, ``` `thumbUri` — the URL of the thumbnail shown in the asset library grid. ```swift highlight-result-thumbUri "thumbUri": image.urls.thumb.absoluteString, ``` `blockType` — the design block to create when the asset is applied. If omitted, the engine infers it from the `mimeType` or by loading the asset data, which delays insertion — so always set it. ```swift highlight-result-blockType "blockType": DesignBlockType.graphic.rawValue, ``` `fillType` — the fill attached to the block. Defaults to a solid color fill when omitted. ```swift highlight-result-fillType "fillType": FillType.image.rawValue, ``` `shapeType` — the shape attached to the block. Defaults to a rectangle when omitted. ```swift highlight-result-shapeType "shapeType": ShapeType.rect.rawValue, ``` `kind` — the kind set on the block. Defaults to an empty string when omitted. ```swift highlight-result-kind "kind": "image", ``` `width` and `height` — the image's original dimensions, used to preserve aspect ratio. ```swift highlight-result-size "width": String(image.width), "height": String(image.height), ``` `looping` — whether the asset loops. Applies only to video and GIF assets. ```swift highlight-result-looping "looping": "false", ``` `context` — contextual information about the asset; currently the source ID it belongs to. ```swift highlight-result-context context: .init(sourceID: "unsplash"), ``` ## Handling Attribution Requirements Unsplash requires you to credit the photographer. Set the per-asset `credits` to the artist's name and a link to their page. ```swift highlight-result-credits credits: .init(name: image.user.name!, url: image.user.links?.html), ``` Some providers also require UTM parameters on every link back to the source or artist. The `utm` field adds a `source` (`utm_source`) and a `medium` (`utm_medium`). ```swift highlight-result-utm utm: .init(source: "CE.SDK Demo", medium: "referral"), ``` Attribution can also be declared once for the whole source. For Unsplash this is a link to the provider and the license that covers every asset from it. ```swift highlight-unsplash-credits-license public var credits: AssetCredits? { .init( name: "Unsplash", url: URL(string: "https://unsplash.com/")!, ) } public var license: AssetLicense? { .init( name: "Unsplash license (free)", url: URL(string: "https://unsplash.com/license")!, ) } ``` ## Adding Download Tracking Unsplash's API guidelines ask you to call a photo's download endpoint whenever an image is used, so photographers receive usage credit. This example maps `image.urls.full` directly and does not trigger that endpoint — the demo proxy's response model doesn't decode the `download_location` link the endpoint needs. To add tracking in production, first extend the response model to decode `download_location` from each photo's `links`. Then request the download endpoint either inside `findAssets` while mapping each result — so the stored `uri` is already the tracked URL — or by overriding the optional `apply(asset:)` hook on `AssetSource` to fire the request when the asset is added to the scene. See [Unsplash's API guidelines](https://help.unsplash.com/en/articles/2511258-guideline-triggering-a-download) for the exact endpoint and requirements. ## Configuring the Asset Library UI On iOS, once the source is registered you can surface it in the editor's asset library. Add it to an image category with `AssetLibrarySource.image(.title("Unsplash"), source: .init(id: UnsplashAssetSource.id))`, which renders the source's results in an image grid. The [Customize Asset Library](#broken-link-c9a4de) guide walks through adding a source to the default library and building a fully custom one. ## Testing the Integration Query the registered source directly to verify it works. Pass an empty query for popular images: ```swift highlight-unsplash-findAssets let list = try await engine.asset.findAssets( sourceID: "ly.img.asset.source.unsplash", query: .init(query: "", page: 1, perPage: 10), ) ``` Pass a search term to hit the search endpoint: ```swift highlight-unsplash-list let search = try await engine.asset.findAssets( sourceID: "ly.img.asset.source.unsplash", query: .init(query: "banana", page: 1, perPage: 10), ) ``` ## Troubleshooting **Rate limiting** — Unsplash enforces per-hour request limits. Cache results and avoid re-querying on every keystroke to stay within them. **Authentication failures** — If requests fail with an authentication error, verify your access key and proxy host (`secrets.unsplashHost`) are configured correctly. **Missing attribution** — Confirm the per-asset `credits` and the source-level `credits` and `license` are populated; the editor uses them to display photographer attribution. **Images not loading** — Check that the mapped `uri` resolves over HTTPS and that App Transport Security permits the host. ## Local Asset Sources When you already have a finite set of assets, you can skip implementing a query callback and let the engine manage search and pagination for you. Create a "local" source with `addLocalSource(sourceID:)` and a unique ID you reference later. ```swift highlight-add-local-source try engine.asset.addLocalSource(sourceID: "background-videos") ``` Add assets with `addAsset(to:asset:)`. The engine stores them and returns matching items from queries, in insertion order. Note that `AssetDefinition` differs from the `AssetResult` returned by queries: it carries all localizations of the labels and tags, whereas an `AssetResult` is specific to the query's locale. ```swift highlight-add-asset-to-source let asset = AssetDefinition(id: "ocean-waves-1", meta: [ "uri": "https://example.com/ocean-waves-1.mp4", "thumbUri": "https://example.com/thumbnails/ocean-waves-1.jpg", "mimeType": "video/mp4", "width": "1920", "height": "1080", ], label: [ "en": "relaxing ocean waves", "es": "olas del mar relajantes", ], tags: [ "en": ["ocean", "waves", "soothing", "slow"], "es": ["mar", "olas", "calmante", "lento"], ]) try engine.asset.addAsset(to: "background-videos", asset: asset) ``` ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addSource(_:)` | Register a custom asset source such as Unsplash. | | `engine.asset.findAssets(sourceID:query:)` | Query a registered source for a page of results. | | `engine.asset.apply(sourceID:assetResult:)` | Add a queried asset to the scene as a design block. | | `engine.asset.addLocalSource(sourceID:)` | Create an engine-managed local asset source. | | `engine.asset.addAsset(to:asset:)` | Add an asset definition to a local source. | ## Next Steps - [Customize Asset Library](#broken-link-c9a4de) — On iOS, configure asset panels and surface custom sources in the editor UI. - [Asset Library Basics](#broken-link-f29078) — On iOS, explore the core functionality of the asset library and how users browse, search, and insert media. - [IMG.LY Premium Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/imgly-premium-assets-eb1688/) — Access a curated set of premium IMG.LY media assets for use in designs. - [Asset Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) — Learn the core asset and import 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: "From Your Server" description: "Serve images, videos, audio, and stickers from your own backend into CE.SDK by implementing a custom asset source or loading a JSON manifest." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/your-server-b91910/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) > [From Your Server](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/your-server-b91910/) --- ```swift file=@cesdk_swift_examples/engine-guides-import-media-from-remote-source-your-server/YourServer.swift reference-only import Foundation import IMGLYEngine // A custom asset source backed by your backend. Implement the `AssetSource` // protocol: expose a unique `id` and a `findAssets(queryData:)` method that // returns paginated results. This example returns a fixed in-memory catalog so // it runs without a network connection; `fetchAssetsFromBackend(...)` below // shows the production shape that requests the same data from your API. final class BackendAssetSource: NSObject, AssetSource { let id: String private let catalog: [AssetResult] var supportedMIMETypes: [String]? { nil } var credits: AssetCredits? { nil } var license: AssetLicense? { nil } init(id: String) { self.id = id let image = AssetResult( id: "photo-1", label: "Mountain Landscape", tags: ["nature", "mountain", "landscape"], meta: [ "uri": "https://cdn.example.com/assets/photo-1.jpg", "thumbUri": "https://cdn.example.com/thumbs/photo-1.jpg", "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "width": "1920", "height": "1080", ], context: .init(sourceID: id), ) let video = AssetResult( id: "clip-1", label: "City Timelapse", tags: ["city", "timelapse"], meta: [ "uri": "https://cdn.example.com/assets/clip-1.mp4", "thumbUri": "https://cdn.example.com/thumbs/clip-1.jpg", "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.video.rawValue, "duration": "12.5", "width": "1920", "height": "1080", ], context: .init(sourceID: id), ) let audio = AssetResult( id: "track-1", label: "Ambient Loop", tags: ["ambient", "loop"], meta: [ "uri": "https://cdn.example.com/assets/track-1.m4a", "thumbUri": "https://cdn.example.com/thumbs/track-1.jpg", "blockType": DesignBlockType.audio.rawValue, "mimeType": "audio/x-m4a", "duration": "30.0", ], context: .init(sourceID: id), ) let sticker = AssetResult( id: "sticker-1", label: "Star Badge", tags: ["badge", "star"], meta: [ "uri": "https://cdn.example.com/assets/sticker-1.png", "thumbUri": "https://cdn.example.com/thumbs/sticker-1.png", "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "kind": "sticker", "width": "512", "height": "512", ], context: .init(sourceID: id), ) catalog = [image, video, audio, sticker] super.init() } func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { let term = (queryData.query ?? "").lowercased() let matches = term.isEmpty ? catalog : catalog.filter { asset in (asset.label?.lowercased().contains(term) ?? false) || (asset.tags?.contains { $0.lowercased().contains(term) } ?? false) } let start = queryData.page * queryData.perPage let page = Array(matches.dropFirst(start).prefix(queryData.perPage)) let hasMore = start + page.count < matches.count return AssetQueryResult( assets: page, currentPage: queryData.page, nextPage: hasMore ? queryData.page + 1 : -1, total: matches.count, ) } } @MainActor func yourServer(engine: Engine) async throws { let source = BackendAssetSource(id: "my-backend") try engine.asset.addSource(source) let results = try await engine.asset.findAssets( sourceID: source.id, query: .init(query: "", page: 0, perPage: 10), ) print("Loaded \(results.assets.count) of \(results.total) assets from the backend source") try engine.asset.addLocalSource(sourceID: "my-backend-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 template = AssetDefinition( id: "promo-card", meta: [ "uri": "https://cdn.example.com/templates/promo-card.scene", "thumbUri": "https://cdn.example.com/thumbs/promo-card.jpg", "blockType": DesignBlockType.scene.rawValue, ], label: ["en": "Promo Card"], ) try engine.asset.addAsset(to: "my-backend-templates", asset: template) let manifest = """ { "version": "1.0.0", "id": "my-backend-stickers", "assets": [ { "id": "star-badge", "label": { "en": "Star Badge" }, "tags": { "en": ["badge", "star"] }, "meta": { "uri": "{{base_url}}/stickers/star-badge.png", "thumbUri": "{{base_url}}/stickers/star-badge.png", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "kind": "sticker", "width": "512", "height": "512" } } ] } """ let staticSourceID = try engine.asset.addLocalAssetSourceFromJSON( manifest, basePath: "https://cdn.example.com/assets", ) print("Registered static source: \(staticSourceID)") } // MARK: - Fetching from your backend // A Decodable model that matches the JSON your `/assets` endpoint returns. private struct BackendAssetPage: Decodable { struct Item: Decodable { let id: String let label: String let uri: String let thumbUri: String let width: Int let height: Int } let assets: [Item] let total: Int let currentPage: Int let nextPage: Int? } // The production shape of `findAssets(queryData:)`: forward the query to your // API, decode the response, and map each row into an `AssetResult`. Called from // `BackendAssetSource.findAssets(queryData:)` in a real integration. @MainActor func fetchAssetsFromBackend( host: URL, sourceID: String, queryData: AssetQueryData, ) async throws -> AssetQueryResult { var components = URLComponents( url: host.appendingPathComponent("assets"), resolvingAgainstBaseURL: false, )! components.queryItems = [ URLQueryItem(name: "query", value: queryData.query), URLQueryItem(name: "page", value: String(queryData.page)), URLQueryItem(name: "perPage", value: String(queryData.perPage)), ] var request = URLRequest(url: components.url!) request.setValue("Bearer YOUR_API_TOKEN", forHTTPHeaderField: "Authorization") let (data, _) = try await URLSession.shared.data(for: request) let page = try JSONDecoder().decode(BackendAssetPage.self, from: data) let assets = page.assets.map { item in AssetResult( id: item.id, label: item.label, meta: [ "uri": item.uri, "thumbUri": item.thumbUri, "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "width": String(item.width), "height": String(item.height), ], context: .init(sourceID: sourceID), ) } return AssetQueryResult( assets: assets, currentPage: page.currentPage, nextPage: page.nextPage ?? -1, total: page.total, ) } ``` Load images, videos, audio, and stickers from your own backend into CE.SDK to integrate a CMS, DAM, or custom asset management system. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-import-media-from-remote-source-your-server) CE.SDK offers two ways to bring your server's content into the engine. Choose the pattern that fits how your assets change. ## Understanding Asset Source Types **Custom asset sources** suit database-backed content — user uploads, DAM integrations, CMS media. You implement the `AssetSource` protocol and provide a `findAssets(queryData:)` method that handles search and pagination against your API. Register it with `engine.asset.addSource(_:)`. **JSON asset sources** suit static content that rarely changes — stickers, icons, brand elements, templates. You host a JSON manifest alongside the assets and register it with `engine.asset.addLocalAssetSourceFromJSON(_:)`. CE.SDK's bundled libraries use this pattern; the [Serve Assets From Your Server](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) guide covers hosting them on your own infrastructure, and [Asset Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) explains how sources and assets fit together. ## Creating a Custom Asset Source A custom source is a class that conforms to `AssetSource`: it exposes a unique `id` and implements `findAssets(queryData:)`. The `queryData` carries the search `query`, the requested `page`, and `perPage`; return an `AssetQueryResult` with the current page of `assets`, the `total` count, the `currentPage`, and `nextPage` (use `-1` when there are no more results). ```swift highlight-yourServer-find-assets func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { let term = (queryData.query ?? "").lowercased() let matches = term.isEmpty ? catalog : catalog.filter { asset in (asset.label?.lowercased().contains(term) ?? false) || (asset.tags?.contains { $0.lowercased().contains(term) } ?? false) } let start = queryData.page * queryData.perPage let page = Array(matches.dropFirst(start).prefix(queryData.perPage)) let hasMore = start + page.count < matches.count return AssetQueryResult( assets: page, currentPage: queryData.page, nextPage: hasMore ? queryData.page + 1 : -1, total: matches.count, ) } ``` Register the source once, then query it through the Asset API: ```swift highlight-yourServer-register let source = BackendAssetSource(id: "my-backend") try engine.asset.addSource(source) let results = try await engine.asset.findAssets( sourceID: source.id, query: .init(query: "", page: 0, perPage: 10), ) print("Loaded \(results.assets.count) of \(results.total) assets from the backend source") ``` The example returns a fixed catalog so it runs without a network connection. In a real integration, `findAssets(queryData:)` builds a request from your backend's base URL, forwards the query, and maps the decoded response into `AssetResult` values. Attach an `Authorization` header for protected endpoints, or sign the asset URLs themselves: ```swift highlight-yourServer-backend-fetch var components = URLComponents( url: host.appendingPathComponent("assets"), resolvingAgainstBaseURL: false, )! components.queryItems = [ URLQueryItem(name: "query", value: queryData.query), URLQueryItem(name: "page", value: String(queryData.page)), URLQueryItem(name: "perPage", value: String(queryData.perPage)), ] var request = URLRequest(url: components.url!) request.setValue("Bearer YOUR_API_TOKEN", forHTTPHeaderField: "Authorization") let (data, _) = try await URLSession.shared.data(for: request) let page = try JSONDecoder().decode(BackendAssetPage.self, from: data) let assets = page.assets.map { item in AssetResult( id: item.id, label: item.label, meta: [ "uri": item.uri, "thumbUri": item.thumbUri, "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "width": String(item.width), "height": String(item.height), ], context: .init(sourceID: sourceID), ) } return AssetQueryResult( assets: assets, currentPage: page.currentPage, nextPage: page.nextPage ?? -1, total: page.total, ) ``` For a complete worked source that talks to a live REST API, see the [Unsplash integration](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source/unsplash-8f31f0/). ## Serving Different Media Types Each media type needs the right `blockType` and `fillType` in its `meta` dictionary. Set `thumbUri` to a small preview and `uri` to the full-resolution asset. ### Image Assets Images use a graphic block with an image fill. Include `width` and `height` so CE.SDK preserves the aspect ratio. ```swift highlight-yourServer-image-asset let image = AssetResult( id: "photo-1", label: "Mountain Landscape", tags: ["nature", "mountain", "landscape"], meta: [ "uri": "https://cdn.example.com/assets/photo-1.jpg", "thumbUri": "https://cdn.example.com/thumbs/photo-1.jpg", "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "width": "1920", "height": "1080", ], context: .init(sourceID: id), ) ``` ### Video Assets Videos use a graphic block with a video fill. Include `duration` for the timeline and `thumbUri` for the preview. ```swift highlight-yourServer-video-asset let video = AssetResult( id: "clip-1", label: "City Timelapse", tags: ["city", "timelapse"], meta: [ "uri": "https://cdn.example.com/assets/clip-1.mp4", "thumbUri": "https://cdn.example.com/thumbs/clip-1.jpg", "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.video.rawValue, "duration": "12.5", "width": "1920", "height": "1080", ], context: .init(sourceID: id), ) ``` ### Audio Assets Audio uses the `//ly.img.ubq/audio` block type. The `mimeType` is required for preview playback; include `duration` for the timeline. ```swift highlight-yourServer-audio-asset let audio = AssetResult( id: "track-1", label: "Ambient Loop", tags: ["ambient", "loop"], meta: [ "uri": "https://cdn.example.com/assets/track-1.m4a", "thumbUri": "https://cdn.example.com/thumbs/track-1.jpg", "blockType": DesignBlockType.audio.rawValue, "mimeType": "audio/x-m4a", "duration": "30.0", ], context: .init(sourceID: id), ) ``` ### Sticker Assets Stickers share the image structure but add `kind: "sticker"`, which marks them as overlays with limited editing options instead of regular images. ```swift highlight-yourServer-sticker-asset let sticker = AssetResult( id: "sticker-1", label: "Star Badge", tags: ["badge", "star"], meta: [ "uri": "https://cdn.example.com/assets/sticker-1.png", "thumbUri": "https://cdn.example.com/thumbs/sticker-1.png", "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "kind": "sticker", "width": "512", "height": "512", ], context: .init(sourceID: id), ) ``` ### Template Assets Templates replace the whole scene rather than adding a block, so they use a local source with a custom `applyAsset` callback that loads a `.scene` file. Return `nil` because applying a template mutates the current scene rather than creating a new block. ```swift highlight-yourServer-template-source try engine.asset.addLocalSource(sourceID: "my-backend-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 template = AssetDefinition( id: "promo-card", meta: [ "uri": "https://cdn.example.com/templates/promo-card.scene", "thumbUri": "https://cdn.example.com/thumbs/promo-card.jpg", "blockType": DesignBlockType.scene.rawValue, ], label: ["en": "Promo Card"], ) try engine.asset.addAsset(to: "my-backend-templates", asset: template) ``` ## Loading Assets from JSON For static collections that rarely change, register a source from a JSON manifest. Each asset carries an `id`, a localized `label`, and a `meta` dictionary. The `{{base_url}}` placeholder resolves against the `basePath` you pass, so the manifest stays portable. ```swift highlight-yourServer-json-source let manifest = """ { "version": "1.0.0", "id": "my-backend-stickers", "assets": [ { "id": "star-badge", "label": { "en": "Star Badge" }, "tags": { "en": ["badge", "star"] }, "meta": { "uri": "{{base_url}}/stickers/star-badge.png", "thumbUri": "{{base_url}}/stickers/star-badge.png", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "kind": "sticker", "width": "512", "height": "512" } } ] } """ let staticSourceID = try engine.asset.addLocalAssetSourceFromJSON( manifest, basePath: "https://cdn.example.com/assets", ) print("Registered static source: \(staticSourceID)") ``` `addLocalAssetSourceFromJSON(_:basePath:matcher:)` takes the manifest inline; the `addLocalAssetSourceFromJSON(_:matcher:)` overload loads a hosted `content.json` from a URL instead. See [Serve Assets From Your Server](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) for the manifest format and hosting details. ## Server Architecture Considerations A few backend choices keep asset loading fast and reliable: - **Authentication** — Pass tokens through your `findAssets` request headers, or hand out signed URLs that embed a temporary access token. - **Thumbnails** — Generate ~512px-wide thumbnails server-side and return them in `meta.thumbUri`; keep the full-resolution asset in `meta.uri`. - **CDN and caching** — Deliver assets through a CDN, with long cache TTLs for immutable thumbnails and shorter TTLs for content that changes. ## Troubleshooting - **Assets not appearing** — Confirm `findAssets(queryData:)` returns a valid `AssetQueryResult` with `assets`, `total`, `currentPage`, and `nextPage`, and that every asset has an `id` and a `meta.uri`. - **Media not loading** — Verify the `uri` is reachable from the device. Apple platforms block plaintext HTTP by default, so serve assets over HTTPS or configure an App Transport Security exception. - **Search returns nothing** — Make sure `findAssets(queryData:)` reads `queryData.query` and forwards the term to your backend. - **Pagination stalls** — Return `nextPage: -1` once results are exhausted, and report the complete result count in `total` rather than the current page size. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addSource(_:)` | Register a custom `AssetSource` whose `findAssets(queryData:)` fetches from your backend. | | `engine.asset.findAssets(sourceID:query:)` | Query a registered source for a page of results. | | `engine.asset.addLocalSource(sourceID:applyAsset:)` | Register a source with a custom apply callback, used for templates. | | `engine.asset.addAsset(to:asset:)` | Add an `AssetDefinition` to a local source. | | `engine.asset.addLocalAssetSourceFromJSON(_:basePath:matcher:)` | Register a source from an inline JSON manifest string. | | `engine.asset.addLocalAssetSourceFromJSON(_:matcher:)` | Register a source from a hosted `content.json` URL. | ### Metadata Properties | Property | Description | | --- | --- | | `uri` | Full-resolution asset URL applied to the canvas. | | `thumbUri` | Thumbnail URL shown in the asset library. | | `blockType` | `//ly.img.ubq/graphic` for images, videos, and stickers; `//ly.img.ubq/audio` for audio. | | `fillType` | `//ly.img.ubq/fill/image` or `//ly.img.ubq/fill/video`. | | `kind` | Set to `sticker` to treat an image asset as a sticker overlay. | | `mimeType` | Required for audio assets, for example `audio/x-m4a`. | | `duration` | Length in seconds for video and audio. | | `width` / `height` | Original dimensions used for aspect-ratio handling. | ## Next Steps - [User Upload](#broken-link-c6c7d9) — On iOS, handle file uploads with progress tracking - [Asset Concepts](https://img.ly/docs/cesdk/mac-catalyst/import-media/concepts-5e6197/) — Asset sources and metadata architecture - [Thumbnails](https://img.ly/docs/cesdk/mac-catalyst/import-media/asset-library/thumbnails-c23949/) — Configure thumbnail display and preview URIs - [Customize Asset Library](#broken-link-c9a4de) — On iOS, present your registered sources in the asset library - [Serve Assets From Your Server](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) — Host CE.SDK's default and sample content on your own infrastructure --- ## 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 import, manage, and customize assets from local, remote, or camera sources in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/overview-84bb23/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/import-media/overview-84bb23/) --- In CE.SDK, assets are the building blocks of your creative workflow—whether they’re images, videos, audio, fonts, or templates. They power everything from basic image edits to dynamic, template-driven design generation. This guide gives you a high-level understanding of how to bring assets into CE.SDK, where they can come from, and how to decide on the right strategy for your application. Whether you're working with local uploads, remote storage, or third-party sources, this guide will help you navigate your options and build an efficient import pipeline. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## File Type Support CreativeEditor SDK (CE.SDK) supports importing high-resolution images, video, and audio content. ## Media Constraints ### Image Resolution Limits ### Video Resolution & Duration Limits --- ## 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: "Retrieve MIME Type" description: "Detect the MIME type of resources loaded in the engine to determine file formats for processing, export, or display." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/retrieve-mimetype-ed13bf/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Retrieve Mimetype](https://img.ly/docs/cesdk/mac-catalyst/import-media/retrieve-mimetype-ed13bf/) --- ```swift file=@cesdk_swift_examples/engine-guides-retrieve-mimetype/RetrieveMimetype.swift reference-only import Foundation import IMGLYEngine @MainActor func retrieveMimetype(engine: Engine) async throws { // Demo scaffolding: resolve a sample image URL and load its bytes so the // example has something concrete to embed and inspect. let baseURL = try engine.guidesBaseURL let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg") let imageData = try await URLSession.shared.data(from: imageURL).0 let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.appendChild(to: scene, child: page) let mimeType = try await engine.editor.getMIMEType(url: imageURL) print("Detected MIME type: \(mimeType)") let imageBuffer = engine.editor.createBuffer() try engine.editor.setBufferData(url: imageBuffer, offset: 0, data: imageData) 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: imageBuffer) try engine.block.setFill(graphic, fill: imageFill) try engine.block.appendChild(to: page, child: graphic) let transientResources = try engine.editor.findAllTransientResources() print("Found \(transientResources.count) transient resources") var resourcesByType: [String: Int] = [:] for resource in transientResources { let type = try await engine.editor.getMIMEType(url: resource.url) resourcesByType[type, default: 0] += 1 } print("Resources by type: \(resourcesByType)") var imageResources: [(url: URL, mimeType: String)] = [] for resource in transientResources { let type = try await engine.editor.getMIMEType(url: resource.url) if type.hasPrefix("image/") { imageResources.append((url: resource.url, mimeType: type)) } } print("Found \(imageResources.count) image resources") let bufferMimeType = try await engine.editor.getMIMEType(url: imageBuffer) let length = try engine.editor.getBufferLength(url: imageBuffer) let data = try engine.editor.getBufferData(url: imageBuffer, offset: 0, length: UInt(truncating: length)) let fileExtension = switch bufferMimeType { case "image/png": "png" case "image/webp": "webp" default: "jpg" } let fileURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension(fileExtension) try data.write(to: fileURL, options: .atomic) print("Saved \(length) bytes as \(fileURL.lastPathComponent)") for resource in transientResources { // Demo placeholder — in production, use the URL your storage service returns. let hostedURL = URL(string: "https://example.com/assets/\(UUID().uuidString)")! try engine.editor.relocateResource(currentURL: resource.url, relocatedURL: hostedURL) } let remaining = try engine.editor.findAllTransientResources() print("Transient resources remaining: \(remaining.count)") } ``` Detect the MIME type of resources the engine can access and relocate embedded media to external URLs using `engine.editor.getMIMEType(url:)` and `engine.editor.relocateResource(currentURL:relocatedURL:)`. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-retrieve-mimetype) When a scene archive bundles its media, the embedded files are held in memory and referenced through internal `buffer://` URIs rather than their original URLs. To process those files correctly — to persist them with the right extension, or to upload them to a CDN and produce a portable scene — you first need to know each resource's format. `getMIMEType(url:)` detects the format of any resource the engine can reach, including buffer URIs, local files, and remote URLs. This guide covers detecting a resource's MIME type, embedding media as an in-memory buffer, finding transient resources, retrieving and filtering their MIME types, reading their bytes, and relocating them to external URLs. ## Detecting the MIME Type Pass any URL the engine can access to `getMIMEType(url:)`. It returns the standard MIME type string — `image/jpeg` for a JPEG, `image/png` for a PNG, and so on — downloading the resource first if it isn't already cached. ```swift highlight-retrieveMimetype-detect let mimeType = try await engine.editor.getMIMEType(url: imageURL) print("Detected MIME type: \(mimeType)") ``` ## Embedding a Resource To work with embedded media, write the raw bytes into an in-memory buffer with `createBuffer()` and `setBufferData(url:offset:data:)`, then use the buffer as a block's image fill. The engine now references the data through a `buffer://` URI — the same kind of resource produced when a scene archive bundles its assets. ```swift highlight-retrieveMimetype-embed let imageBuffer = engine.editor.createBuffer() try engine.editor.setBufferData(url: imageBuffer, offset: 0, data: imageData) 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: imageBuffer) try engine.block.setFill(graphic, fill: imageFill) try engine.block.appendChild(to: page, child: graphic) ``` ## Finding Transient Resources Transient resources are embedded files whose data would be lost if the scene were exported on its own. `findAllTransientResources()` returns each resource's `url` and `size` in bytes. ```swift highlight-retrieveMimetype-findTransient let transientResources = try engine.editor.findAllTransientResources() print("Found \(transientResources.count) transient resources") ``` ## Retrieving MIME Types Call `getMIMEType(url:)` on each transient resource to categorize what the scene embeds. This is useful for reporting on a scene's contents or deciding how to process each resource. ```swift highlight-retrieveMimetype-getMimetype var resourcesByType: [String: Int] = [:] for resource in transientResources { let type = try await engine.editor.getMIMEType(url: resource.url) resourcesByType[type, default: 0] += 1 } print("Resources by type: \(resourcesByType)") ``` A scene may embed several formats at once — images alongside fonts and audio. The method returns standard MIME type strings such as `image/jpeg`, `image/png`, `font/ttf`, or `font/woff2`. ## Filtering Resources by Type To process only one kind of resource, filter by the MIME type prefix. This separates image handling from font or audio handling. ```swift highlight-retrieveMimetype-filterImages var imageResources: [(url: URL, mimeType: String)] = [] for resource in transientResources { let type = try await engine.editor.getMIMEType(url: resource.url) if type.hasPrefix("image/") { imageResources.append((url: resource.url, mimeType: type)) } } print("Found \(imageResources.count) image resources") ``` ## Reading Buffer Data Once you know a buffer's MIME type, read its bytes with `getBufferLength(url:)` and `getBufferData(url:offset:length:)`. Use the MIME type to choose the correct file extension when persisting the bytes to disk. ```swift highlight-retrieveMimetype-bufferData let bufferMimeType = try await engine.editor.getMIMEType(url: imageBuffer) let length = try engine.editor.getBufferLength(url: imageBuffer) let data = try engine.editor.getBufferData(url: imageBuffer, offset: 0, length: UInt(truncating: length)) let fileExtension = switch bufferMimeType { case "image/png": "png" case "image/webp": "webp" default: "jpg" } let fileURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension(fileExtension) try data.write(to: fileURL, options: .atomic) print("Saved \(length) bytes as \(fileURL.lastPathComponent)") ``` ## Relocating Resources `relocateResource(currentURL:relocatedURL:)` updates every reference to a resource's current URL so the scene points at a new URL instead. After uploading the bytes you read to your storage service, relocate each `buffer://` URI to the hosted URL it returns. This produces a scene that references external assets instead of embedding their data. ```swift highlight-retrieveMimetype-relocate for resource in transientResources { // Demo placeholder — in production, use the URL your storage service returns. let hostedURL = URL(string: "https://example.com/assets/\(UUID().uuidString)")! try engine.editor.relocateResource(currentURL: resource.url, relocatedURL: hostedURL) } ``` ## Verifying Relocation After relocating every embedded resource to a hosted URL, `findAllTransientResources()` returns an empty array — the scene no longer carries data that would be lost on export. ```swift highlight-retrieveMimetype-verify let remaining = try engine.editor.findAllTransientResources() print("Transient resources remaining: \(remaining.count)") ``` ## API Reference | Method | Description | | --- | --- | | `engine.editor.getMIMEType(url:)` | Returns the MIME type of the resource at the given URL, downloading it first if it isn't cached. | | `engine.editor.findAllTransientResources()` | Returns the `url` and byte `size` of every resource whose data would be lost on export, such as embedded buffers. | | `engine.editor.getBufferLength(url:)` | Returns the byte length of a buffer as an `NSNumber`. | | `engine.editor.getBufferData(url:offset:length:)` | Returns raw `Data` from a buffer, starting at `offset` for `length` bytes. | | `engine.editor.relocateResource(currentURL:relocatedURL:)` | Updates every reference to `currentURL` in the scene to use `relocatedURL` instead. | ## Troubleshooting ### MIME Type Cannot Be Determined `getMIMEType(url:)` throws when the URL can't be parsed or the resource can't be fetched — a missing file or an unreachable URL, for example. For a resource the engine reaches but can't classify, it returns the string `"unknown"`. Wrap the call in `do`/`catch`, and treat a thrown error or an `"unknown"` result as the cue to fall back to a default type. ### No Transient Resources Found If `findAllTransientResources()` returns an empty array, the scene has no embedded media. Its resources may already reference external URLs, or no buffer-backed fills were added. ### Resources Not Relocated If transient resources remain after `relocateResource(currentURL:relocatedURL:)`, confirm you pass the exact URL returned by `findAllTransientResources()`. Bundle resources (`bundle://`) are internal and cannot be relocated. ## Next Steps - [Import Design from Archive](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-archive-dde9fa/) — Load self-contained archive files that bundle a scene with all of its embedded assets. - [Export Options](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Export scenes and work with the resulting resource data. --- ## 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: "Learn about file size restrictions and how to optimize large assets for use in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/size-limits-c32275/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Size Limits](https://img.ly/docs/cesdk/mac-catalyst/import-media/size-limits-c32275/) --- CreativeEditor SDK (CE.SDK) supports importing high-resolution images, video, and audio, but there are practical limits to consider based on the user's device capabilities. ## Image Resolution Limits ## Video Resolution & Duration Limits --- ## 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: "Source Sets" description: "Provide multiple versions of images and videos at different resolutions for optimal performance and quality across editing and export workflows." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/import-media/source-sets-5679c8/" --- > 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/) > [Import Media Assets](https://img.ly/docs/cesdk/mac-catalyst/import-media-4e3703/) > [Source Sets](https://img.ly/docs/cesdk/mac-catalyst/import-media/source-sets-5679c8/) --- ```swift file=@cesdk_swift_examples/engine-guides-source-sets/SourceSets.swift reference-only import Foundation import IMGLYEngine @MainActor func sourceSets(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: 50, paddingTop: 50, paddingRight: 50, paddingBottom: 50) let baseURL = try engine.guidesBaseURL let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let imageFill = try engine.block.createFill(.image) try engine.block.setSourceSet(imageFill, property: "fill/image/sourceSet", sourceSet: [ .init(uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-512x341.jpg"), width: 512, height: 341), .init(uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1249x833.jpg"), width: 1249, height: 833), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1767x1178.jpg"), width: 1767, height: 1178, ), ]) try engine.block.setFill(block, fill: imageFill) try engine.block.appendChild(to: page, child: block) let sources = try engine.block.getSourceSet(imageFill, property: "fill/image/sourceSet") print("Image source set has \(sources.count) sources") try await engine.block.addImageFileURIToSourceSet( imageFill, property: "fill/image/sourceSet", uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) let assetWithSourceSet = AssetDefinition( id: "my-image", meta: [ "kind": "image", "fillType": "//ly.img.ubq/fill/image", ], payload: .init(sourceSet: [ .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-512x341.jpg"), width: 512, height: 341, ), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1249x833.jpg"), width: 1249, height: 833, ), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1767x1178.jpg"), width: 1767, height: 1178, ), ]), ) try engine.asset.addLocalSource(sourceID: "my-dynamic-images") try engine.asset.addAsset(to: "my-dynamic-images", asset: assetWithSourceSet) // In an app, look the asset up from its source with `findAssets` or `fetchAsset`. // Here we build the `AssetResult` directly because we already have the definition. let assetResult = AssetResult( id: assetWithSourceSet.id, meta: assetWithSourceSet.meta, context: AssetContext(sourceID: "my-dynamic-images"), ) if let appliedBlock = try await engine.asset.defaultApplyAsset(assetResult: assetResult) { let appliedFill = try engine.block.getFill(appliedBlock) let appliedSources = try engine.block.getSourceSet(appliedFill, property: "fill/image/sourceSet") print("Applied block fill has \(appliedSources.count) sources") } let videoFill = try engine.block.createFill(.video) try engine.block.setSourceSet(videoFill, property: "fill/video/sourceSet", sourceSet: [ .init( uri: baseURL.appendingPathComponent("ly.img.video/videos/pexels-kampus-production-8154913.mp4"), width: 720, height: 1280, ), ]) try await engine.block.addVideoFileURIToSourceSet( videoFill, property: "fill/video/sourceSet", uri: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) try engine.editor.setSettingBool("features/forceLowQualityVideoPreview", value: true) } ``` Configure source sets for images and videos so CE.SDK automatically selects the optimal resolution for editing previews and exports. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-source-sets) Source sets let you provide multiple versions of the same asset at different resolutions. CE.SDK automatically selects the most appropriate source based on the current drawing size in screen pixels. This improves performance by loading smaller images for mobile previews while ensuring high-quality assets are used for final exports. This guide covers how to configure source sets programmatically, define them in asset definitions, and optimize video preview performance. ## How Source Set Selection Works When rendering content, the engine calculates the current drawing size in pixels. If a source set exists, the engine selects the source with the closest size exceeding the drawing size. If no source set is defined, the full resolution image is downscaled to a maximum 4096px edge length (configurable via the `maxImageSize` setting). Source sets are also evaluated during export, ensuring the best matching asset is used for the target export resolution. Because every intermediate resolution is one you supply, you stay in control of how content is up- and downsampled. ## Setting a Source Set on an Image Fill Configure a source set for an image fill with `setSourceSet`. Each source entry requires a `uri`, `width`, and `height`, and the engine uses these dimensions to select the appropriate source while drawing. > **Caution:** CE.SDK provides two ways to set image content: the `fill/image/imageFileURI` property for a single image, or source sets for multiple resolutions. Use one or the other—setting both on the same fill leads to undefined behavior. ```swift highlight-set-source-set let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let imageFill = try engine.block.createFill(.image) try engine.block.setSourceSet(imageFill, property: "fill/image/sourceSet", sourceSet: [ .init(uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-512x341.jpg"), width: 512, height: 341), .init(uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1249x833.jpg"), width: 1249, height: 833), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1767x1178.jpg"), width: 1767, height: 1178, ), ]) try engine.block.setFill(block, fill: imageFill) try engine.block.appendChild(to: page, child: block) ``` ## Querying and Modifying Source Sets Retrieve an existing source set with `getSourceSet`. To add sources dynamically, use `addImageFileURIToSourceSet`, which loads the image to determine its dimensions automatically. Provide the dimensions up front with `setSourceSet` when they are already known to avoid the extra fetch. ```swift highlight-query-source-set let sources = try engine.block.getSourceSet(imageFill, property: "fill/image/sourceSet") print("Image source set has \(sources.count) sources") try await engine.block.addImageFileURIToSourceSet( imageFill, property: "fill/image/sourceSet", uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) ``` ## Using Source Sets in Asset Definitions When defining assets for the asset library, include a source set in the `payload.sourceSet` field. When the asset is applied with `defaultApplyAsset`, the source set is automatically configured on the resulting block's fill. The natural way to retrieve a registered asset from its source is `findAssets` or `fetchAsset`. The example builds the `AssetResult` directly because it already has the definition in hand. ```swift highlight-asset-source-set let assetWithSourceSet = AssetDefinition( id: "my-image", meta: [ "kind": "image", "fillType": "//ly.img.ubq/fill/image", ], payload: .init(sourceSet: [ .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-512x341.jpg"), width: 512, height: 341, ), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1249x833.jpg"), width: 1249, height: 833, ), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1767x1178.jpg"), width: 1767, height: 1178, ), ]), ) try engine.asset.addLocalSource(sourceID: "my-dynamic-images") try engine.asset.addAsset(to: "my-dynamic-images", asset: assetWithSourceSet) // In an app, look the asset up from its source with `findAssets` or `fetchAsset`. // Here we build the `AssetResult` directly because we already have the definition. let assetResult = AssetResult( id: assetWithSourceSet.id, meta: assetWithSourceSet.meta, context: AssetContext(sourceID: "my-dynamic-images"), ) if let appliedBlock = try await engine.asset.defaultApplyAsset(assetResult: assetResult) { let appliedFill = try engine.block.getFill(appliedBlock) let appliedSources = try engine.block.getSourceSet(appliedFill, property: "fill/image/sourceSet") print("Applied block fill has \(appliedSources.count) sources") } ``` ## Video Source Sets Source sets also work with video fills using the `fill/video/sourceSet` property. The engine selects the appropriate video source based on the current drawing size, and `addVideoFileURIToSourceSet` adds video sources dynamically. ```swift highlight-video-source-set let videoFill = try engine.block.createFill(.video) try engine.block.setSourceSet(videoFill, property: "fill/video/sourceSet", sourceSet: [ .init( uri: baseURL.appendingPathComponent("ly.img.video/videos/pexels-kampus-production-8154913.mp4"), width: 720, height: 1280, ), ]) try await engine.block.addVideoFileURIToSourceSet( videoFill, property: "fill/video/sourceSet", uri: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ), ) ``` ## Video Preview Quality Settings For low-end devices or scenes with large videos, force the engine to use the smallest available source for video previews during editing. Export operations always use the highest quality source. ```swift highlight-video-preview-settings try engine.editor.setSettingBool("features/forceLowQualityVideoPreview", value: true) ``` The `features/forceLowQualityVideoPreview` setting forces previews to use the smallest source while editing. It is disabled by default, so the engine uses the source closest to the current drawing size. Thumbnails use the smallest source unless `features/matchThumbnailSourceToFill` is enabled, which is also disabled by default. ## Troubleshooting | Problem | Solution | |---------|----------| | Wrong resolution selected | Ensure source dimensions accurately reflect actual image/video dimensions | | Performance issues with large assets | Add smaller resolution sources to your source set for editing preview | | Export quality issues | Verify that your source set includes a high-resolution source for the target export size | | Source set not applied from asset | Ensure `payload.sourceSet` is defined with valid `uri`, `width`, and `height` entries | ## API Reference ### Methods | Method | Description | |--------|-------------| | `engine.block.setSourceSet(_:property:sourceSet:)` | Set a source set for a block property | | `engine.block.getSourceSet(_:property:)` | Get the source set from a block property | | `engine.block.addImageFileURIToSourceSet(_:property:uri:)` | Add an image to an existing source set (async) | | `engine.block.addVideoFileURIToSourceSet(_:property:uri:)` | Add a video to an existing source set (async) | | `engine.block.createFill(_:)` | Create an image or video fill | | `engine.block.setFill(_:fill:)` | Apply a fill to a block | | `engine.block.getFill(_:)` | Get the fill from a block | | `engine.asset.addLocalSource(sourceID:)` | Create a local asset source | | `engine.asset.addAsset(to:asset:)` | Add an asset with a source set to a source | | `engine.asset.defaultApplyAsset(assetResult:)` | Apply an asset, configuring its source set | | `engine.editor.setSettingBool(_:value:)` | Configure editor settings like video preview quality | ### Properties | Property | Type | Description | |----------|------|-------------| | `fill/image/sourceSet` | `[Source]` | Source set for an image fill | | `fill/video/sourceSet` | `[Source]` | Source set for a video fill | | `features/forceLowQualityVideoPreview` | Bool | Force the smallest source during video editing previews | | `features/matchThumbnailSourceToFill` | Bool | Match the thumbnail source to the fill instead of using the smallest | --- ## 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: "Insert Media Into Scenes" description: "Understand how insertion works, how inserted media behave within scenes, and how to control them via UI or code." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/insert-media-a217f5/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/mac-catalyst/insert-media-a217f5/) --- --- ## Related Pages - [Insert Media Overview](https://img.ly/docs/cesdk/mac-catalyst/insert-media/overview-491658/) - Place images, videos, audio, shapes, and stickers into CE.SDK scenes from Swift, and understand how inserted media behave on the timeline and during export. - [Insert Images](https://img.ly/docs/cesdk/mac-catalyst/insert-media/images-63848a/) - Add still images to CE.SDK scenes programmatically using Swift or using the built-in iOS editor UI. Includes positioning, layering, sizing and format considerations. - [Insert Videos](https://img.ly/docs/cesdk/mac-catalyst/insert-media/videos-a5fa03/) - Insert videos into CE.SDK scenes programmatically using the Engine API for Swift. - [Insert Audio](https://img.ly/docs/cesdk/mac-catalyst/insert-media/audio-c10f10/) - Insert audio tracks into CE.SDK scenes programmatically using the Engine API for Swift. - [Insert Shapes or Stickers](https://img.ly/docs/cesdk/mac-catalyst/insert-media/shapes-or-stickers-20ac68/) - Add shapes and stickers to your designs using CE.SDK. Create rectangles, ellipses, stars, polygons, lines, and custom vector paths 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: "Insert Audio" description: "Insert audio tracks into CE.SDK scenes programmatically using the Engine API for Swift." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/insert-media/audio-c10f10/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/mac-catalyst/insert-media-a217f5/) > [Insert Audio](https://img.ly/docs/cesdk/mac-catalyst/insert-media/audio-c10f10/) --- Add audio files to your CE.SDK scenes programmatically with the Swift Engine API: create audio blocks, configure timeline position, and control playback properties for background music, voiceovers, and sound effects. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-insert-media-audio) Audio blocks are time-based blocks that play sound alongside the rest of a scene. They have no visual canvas representation — they live on the timeline with their own duration, offset, and volume controls, independent of the video fills attached to graphic blocks. ```swift file=@cesdk_swift_examples/engine-guides-insert-media-audio/InsertMediaAudio.swift reference-only import Foundation import IMGLYEngine @MainActor func insertMediaAudio(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: 30) let baseURL = try engine.guidesBaseURL // Create an audio block, point it at an audio file, and append it to a page. 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) // Start playback at the beginning of the timeline and clamp the duration to // the page length or the source file, whichever is shorter. let totalDuration = try engine.block.getAVResourceTotalDuration(audioBlock) try engine.block.setTimeOffset(audioBlock, offset: 0) try engine.block.setDuration(audioBlock, duration: min(totalDuration, 30)) // Set the audio level. Volume is a Float ranging from 0.0 (silent) to 1.0 (full). try engine.block.setVolume(audioBlock, volume: 0.8) let currentVolume = try engine.block.getVolume(audioBlock) print(String(format: "Audio volume: %.0f%%", currentVolume * 100)) // Silence the block without changing the configured volume, then read the state back. try engine.block.setMuted(audioBlock, muted: true) let muted = try engine.block.isMuted(audioBlock) print("Audio muted: \(muted)") // Enable looping so the source repeats until the block's timeline duration ends. try engine.block.setLooping(audioBlock, looping: true) let looping = try engine.block.isLooping(audioBlock) print("Audio looping: \(looping)") // 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 the block to remove it from the scene and free its resources. try engine.block.destroy(audioBlock) } ``` This guide covers creating audio blocks, configuring their time-based properties, controlling playback, and managing audio blocks in a scene. For broader audio workflows, see the [Create and Edit Audio overview](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio-2f700b/). For catalog-based music selection and multiple background tracks, continue with [Add Music](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-music-5b182c/). ## Insert an Audio File Create an audio block with `create(_:)`, set its source file URL with `setURL(_:property:value:)` using the `audio/fileURI` property, then append it to a page with `appendChild(to:child:)`. Audio blocks must be children of a page to participate in the timeline. ```swift highlight-insertMediaAudio-createAudioBlock // Create an audio block, point it at an audio file, and append it to a page. 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 sample resolves the demo track against the SDK's asset base URL, so it stays aligned with the packaged guide assets or your self-hosted assets. You can also pass a reachable remote URL or a local file URL resolved by your app. CE.SDK supports M4A, MP3, and WAV formats. ## Configuring Time Position Audio blocks have time-based properties that control when and how long they play. Use `setTimeOffset(_:offset:)` for the start position and `setDuration(_:duration:)` for playback length. Call `forceLoadAVResource(_:)` first to ensure the audio file is loaded before reading metadata such as total duration. ```swift highlight-insertMediaAudio-configureTimeline // Wait for the audio resource to load before reading metadata such as duration. try await engine.block.forceLoadAVResource(audioBlock) // Start playback at the beginning of the timeline and clamp the duration to // the page length or the source file, whichever is shorter. let totalDuration = try engine.block.getAVResourceTotalDuration(audioBlock) try engine.block.setTimeOffset(audioBlock, offset: 0) try engine.block.setDuration(audioBlock, duration: min(totalDuration, 30)) ``` `getAVResourceTotalDuration(_:)` returns the length of the source audio file in seconds. Use it to clamp the playback duration to the available content or to compute timing relative to the file length. ## Adjusting Volume Set the audio level with `setVolume(_:volume:)`. Volume is a `Float` between `0.0` (silent) and `1.0` (full volume) and applies to both preview playback and the exported output. ```swift highlight-insertMediaAudio-adjustVolume // Set the audio level. Volume is a Float ranging from 0.0 (silent) to 1.0 (full). try engine.block.setVolume(audioBlock, volume: 0.8) let currentVolume = try engine.block.getVolume(audioBlock) print(String(format: "Audio volume: %.0f%%", currentVolume * 100)) ``` Read the current level back with `getVolume(_:)`. For a deeper look at mixing and force-mute behavior, see [Adjust Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/). ## Muting Audio To silence a block without changing its configured volume, use `setMuted(_:muted:)`. Muting preserves the volume value so you can restore the previous level by setting `muted` back to `false`. ```swift highlight-insertMediaAudio-muteAudio // Silence the block without changing the configured volume, then read the state back. try engine.block.setMuted(audioBlock, muted: true) let muted = try engine.block.isMuted(audioBlock) print("Audio muted: \(muted)") ``` Read the current state with `isMuted(_:)`. ## Looping Audio Enable continuous playback with `setLooping(_:looping:)`. When looping is enabled, the source repeats until the end of the block's timeline duration is reached. ```swift highlight-insertMediaAudio-loopAudio // Enable looping so the source repeats until the block's timeline duration ends. try engine.block.setLooping(audioBlock, looping: true) let looping = try engine.block.isLooping(audioBlock) print("Audio looping: \(looping)") ``` Read the current state with `isLooping(_:)`. ## Finding 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 such as adjusting levels across all tracks at once. ```swift highlight-insertMediaAudio-findAudioBlocks // 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)) } ``` For each block, read the source URI with `getString(_:property:)` and the timeline properties with `getTimeOffset(_:)`, `getDuration(_:)`, and `getVolume(_:)`. ## Removing 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-insertMediaAudio-removeAudio // Destroy the block to remove it from the scene and free its resources. try engine.block.destroy(audioBlock) ``` ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `engine.block.create(.audio)` | Block | Create a new audio block | | `engine.block.setURL(_:property:value:)` (`audio/fileURI`) | Block | Set the audio source file | | `engine.block.getString(_:property:)` (`audio/fileURI`) | Block | Get the audio source file | | `engine.block.appendChild(to:child:)` | Block | Add audio to a page | | `engine.block.forceLoadAVResource(_:)` | Block | Load audio metadata | | `engine.block.getAVResourceTotalDuration(_:)` | Block | Get total audio file duration in seconds | | `engine.block.setTimeOffset(_:offset:)` | Block | Set timeline start position | | `engine.block.getTimeOffset(_:)` | Block | Get timeline start position | | `engine.block.setDuration(_:duration:)` | Block | Set playback duration | | `engine.block.getDuration(_:)` | Block | Get playback duration | | `engine.block.setVolume(_:volume:)` | Block | Set volume (`0.0`–`1.0`) | | `engine.block.getVolume(_:)` | Block | Get current volume | | `engine.block.setMuted(_:muted:)` | Block | Mute or unmute audio | | `engine.block.isMuted(_:)` | Block | Check if audio is muted | | `engine.block.setLooping(_:looping:)` | Block | Enable or disable looping | | `engine.block.isLooping(_:)` | Block | Check if looping is enabled | | `engine.block.find(byType:)` | Block | Find all audio blocks | | `engine.block.destroy(_:)` | Block | Remove an audio block | ## Next Steps - [Adjust Audio Volume](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/adjust-volume-7ecc4a/) — Fine-tune audio levels and balance multiple sources - [Loop Audio](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/loop-937be7/) — Repeat a source for the duration of its audio block - [Add Sound Effects](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-sound-effects-9e984e/) — Generate procedural sound effects from PCM-backed audio buffers - [Add Music](https://img.ly/docs/cesdk/mac-catalyst/create-audio/audio/add-music-5b182c/) — Add background music tracks to video projects - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Export scenes with audio to 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: "Insert Images" description: "Add still images to CE.SDK scenes programmatically using Swift or using the built-in iOS editor UI. Includes positioning, layering, sizing and format considerations." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/insert-media/images-63848a/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/mac-catalyst/insert-media-a217f5/) > [Insert Images](https://img.ly/docs/cesdk/mac-catalyst/insert-media/images-63848a/) --- You can insert images into a scene using CE.SDK, either through the prebuilt UI for iOS or programmatically via Swift for all platforms. This gives you the flexibility to build interactive design workflows, enable user-generated content, or automate image placement based on logic or data. > **Note:** CE.SDK supports a wide range of image formats, including:* `.png` > * `.jpeg`, `.jpg` > * `.gif` > * `.webp` > * `.svg` > * `.bmp`See a [full list](https://img.ly/docs/cesdk/mac-catalyst/file-format-support-3c4b2a/) of supported file types. ## What You’ll Learn - Two ways to insert images: - Programmatically (iOS/macOS/catalyst) by creating a graphic block, applying an image fill, and setting its position/size/rotation/z-index. - With Editor UI (iOS Only) using the controls and asset libraries of a prebuilt editor such as the Design Editor or Photo Editor. - Supported image sources such as bundled assets, app file URLs, and remote URLs. - Practical transforms after insertion such as move, scale, rotate and order. ## When to Use It - You’re building custom UI or automation flow to add images to compositions. - You want a ready-made editing experience on iOS with an image picker and panels. > **Note:** Prefer the programmatic approach and custom UI on macOS/Catalyst/iPad. Use the prebuilt editors on the iPhone only. ## Inserting Images Using the UI CE.SDK’s UI includes a built-in **image tool** that lets users add images from device sources directly onto the canvas. Once inserted, users can move, resize, crop, rotate, or stack images visually. Image controls on the IMGLY UI **Supported image sources:** - Photo Roll (Photos app) - Disk (Files app) - Camera (device camera) - Image (project asset library) In the Asset Library, a user can add images from the Photos app, the camera or the Files app. Add button in the Asset Library You can customize how the image tool appears in the user interface. ## Inserting Images Programmatically For apps with automation, batch workflows, or logic-driven design experiences, you can insert images into a scene using the block API and the graphics engine. Here’s how to do it: ```swift // 1. Create a graphic block let imageBlock = try engine.block.create(.graphic) // 2. Create a shape for the image let shape = try engine.block.createShape(.rect) try engine.block.setShape(imageBlock, shape: shape) // 3. Create an image fill let imageFill = try engine.block.createFill(.image) try engine.block.setString( imageFill, property: "fill/image/imageFileURI", value: "https://img.ly/static/ubq_samples/sample_4.jpg" ) try engine.block.setFill(imageBlock, fill: imageFill) // 4. (Optional) Set semantic kind to "image" for clarity try engine.block.setKind(imageBlock, kind: "image") // 5. Add image to the scene let page = try engine.block.find(byType: .page).first! try engine.block.appendChild(page, child: imageBlock) ``` The `shape` can be any of the supported shapes `.rect`, `.star`, etc and masks the inserted image. The asset URI in step 3 can either be a remote URL or a local asset URI represented as a String. For assets in the app bundle, get a URL: ```swift let url = Bundle.main.url(forResource: "poster", withExtension: "jpg") ``` For file assets, use the standard `FileManager`: ```swift let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let file = docs.appendingPathComponent("uploads/avatar.png") ``` When working with the asset catalog, you can apply an image that’s an `AssetResult` either to: - The scene directly - A block In the code below `assetList` is an `AssetQueryResult` which is the result of a call to `findAssets` to get assets from an asset catalog. ```swift guard let newAsset = assetList.assets.first else { return } // Creates a new block that contains the image let imageBlock = try await engine.asset.defaultApplyAsset(assetResult: newAsset) // Applies the image to a block that already exists try await engine.asset.defaultApplyAssetToBlock(assetResult: newAsset, block: someBlock) ``` ## Image Properties After inserting the image, you can change the block's layout properties using standard methods in the `engine.block` API. ### Positioning Refer to the guide in the Transform Section for [Move](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/move-818dd9/) for more details and other options. ```swift // Set X/Y position on the canvas (in absolute units) try engine.block.setPositionX(imageBlock, value: 100) try engine.block.setPositionY(imageBlock, value: 200) ``` ### Scaling Refer to the guide in the Transform Section for [Scale](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/scale-ebe367/) for more details and other options. ```swift // Uniform scale try engine.block.setFloat(imageBlock, property: "transform/scale/x", value: 1.5) try engine.block.setFloat(imageBlock, property: "transform/scale/y", value: 1.5) // Non-uniform (stretching) try engine.block.setFloat(imageBlock, property: "transform/scale/x", value: 2.0) try engine.block.setFloat(imageBlock, property: "transform/scale/y", value: 1.0) ``` ### Rotation Refer to the guide in the Transform Section for [Rotate](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform/rotate-5f39c9/) for more details and other options. ```swift // Rotate 45 degrees (in radians) let degrees = 45.0 let radians = degrees * (.pi / 180) try engine.block.setFloat(imageBlock, property: "transform/rotation", value: Float(radians)) ``` ### Layering Control stack order using the helper methods to move blocks forward (towards the user) or backwards. You can also pin a block to the front or back of the stack. ```swift try engine.block.bringToFront(block) // Move above siblings try engine.block.sendToBack(block) // Move below siblings try engine.block.bringForward(block) // One step forward try engine.block.sendBackward(block) // One step backward try engine.block.setAlwaysOnTop(block, enabled: true) ``` > **Note:** You can also group images and other elements using `engine.block.group()` for easier layer management. ## Insert Into an Existing Block If your template exposes a placeholder block or you are creating an automated workflow, you can **replace an image fill** instead of creating a new block. Locate the block using its `name` property (this pairs well with the process for text variables) or by its known `id`. When you know the `id` of the target: ```swift let fill = try engine.block.createFill(.image) try engine.block.setString(fill, property: "fill/image/imageFileURI", value: imageURI) try engine.block.setFill(targetBlock, fill: fill) ``` When you’re using the `name` property to find the block, `find(byName:)` returns the block `id`: ```swift guard let targetBlock = engine.block.find(byName: "HeroTile") else { return } let fill = try engine.block.createFill(.image) try engine.block.setString(fill, property: "fill/image/imageFileURI", value: imageURI) try engine.block.setFill(targetBlock, fill: fill) ``` > **Note:** When generating templates, assign names so downstream replacement stays straightforward:```swift > try engine.block.setString(imageBlock, property: "name", value: "HeroImage") > ``` ## Troubleshooting **❌ Nothing appears after insert**: - Verify that the block is attached to the page. - Verify the URL string is correct (use `.absoluteString` property). - Check the scene’s current zoom and camera framing. **❌ Remote images fail**: - Confirm HTTPS, CORS, or ATS settings. - Test the URL in a browser. **❌ Pixelated result**: - Change the block size or use a higher-resolution source image. **❌ Unexpected orientation of image**: - Some formats carry EXIF orientation information. Apply `setRotation` or normalize the asset during import. ## Next Steps Now that you’ve learned about inserting images into your compositions, here are some topics to explore to deepen your understanding. - Apply more [transformations](https://img.ly/docs/cesdk/mac-catalyst/edit-image/transform-9d189b/) such as crop, or scale. - Create [templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates-3aef79/) for automating content creation and formatting. - [Export](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export-82f968/) compositions in a variety of 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: "Insert Media Overview" description: "Place images, videos, audio, shapes, and stickers into CE.SDK scenes from Swift, and understand how inserted media behave on the timeline and during export." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/insert-media/overview-491658/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/mac-catalyst/insert-media-a217f5/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/insert-media/overview-491658/) --- See how inserted media becomes part of a CE.SDK scene — what kinds of blocks back each media type, how the editor UI and the Engine API place them, and how saved scenes carry references versus embedded bytes. Inserting media turns an asset — a photo, a video clip, an audio file, a shape, a sticker — into a design block that lives in the scene graph. Once inserted, the asset behaves like any other block in the scene: it has a `DesignBlockID`, occupies a position on a page or timeline, carries its own size, rotation, opacity, and styling, and participates in save, reload, and export. Use this overview to build a mental model of how insertion fits into CE.SDK scenes before reaching for the focused sub-guides. The Engine API works the same way across iOS, macOS, and Mac Catalyst. On iOS, the editor UI gives users a visual surface for the same insertion flows. ## Inserting Media vs. Importing Assets *Importing* an asset registers it with the asset library so it can be looked up or browsed later. Use `engine.asset.addLocalSource(sourceID:...)` to register a custom asset source, or `engine.asset.addLocalAssetSourceFromJSON(_:)` to register an entire JSON manifest of assets. Importing alone does not place anything on the canvas. *Inserting* places media into the scene as a design block. Image and video assets typically become the fill of a `.graphic` block — create the block with `engine.block.create(.graphic)`, build the fill with `engine.block.createFill(.image)` (or `.video`), and attach it with `engine.block.setFill(_:fill:)`. Audio files become standalone `.audio` blocks. Shapes and stickers become `.graphic` blocks with a custom shape and a solid or vector fill. The two steps are independent. You can insert media without importing it first (point a fill directly at a URL or a file on disk), and you can import assets without ever inserting any of them (populate an asset panel that the user picks from later). ## How Media Is Handled in Scenes Inserted media lives inside the scene graph as design blocks. The shape depends on the media type: - **Images and videos** are stored as the *fill* of a `.graphic` block. The block defines the position, size, rotation, and opacity; the fill carries the source URI and, for video, the playback offsets. Set the URI with `engine.block.setString(_:property:value:)` using the property keys `fill/image/imageFileURI` or `fill/video/fileURI`. - **Audio** is a `.audio` block appended directly to a page. It has no visual representation but participates in the page timeline through its time offset, duration, volume, and looping flag. - **Shapes** are `.graphic` blocks paired with a shape child block — `rect`, `ellipse`, `star`, `polygon`, `line`, or `vector_path` — created via `engine.block.createShape(_:)` and attached with `engine.block.setShape(_:shape:)`. The block carries a color, gradient, or image fill that the shape outlines. - **Stickers** are `.graphic` blocks with a rect shape and an image fill pointed at a sticker asset, typically a transparent PNG or SVG. They behave like image blocks for positioning, ordering, and styling. Every inserted block exposes a `DesignBlockID` that you can store, query, and pass back into the Engine to read or modify its properties. ## Inserting Media ### Insert via the UI On iOS, the editor UI exposes asset-library panels, drag-and-drop targets, and quick-add buttons that let users place media without writing code. Configure the available categories and sources through your editor configuration, and open an asset sheet from a dock or inspector item by sending an `EditorEvent.openSheet(...)`. The user's selection drives the insertion: the editor creates the block, sets reasonable defaults, and selects it for further editing. On native macOS, the editor UI does not ship — drive insertion programmatically from the Engine API and surface it through your own AppKit or SwiftUI views. To control which categories and sources users see in the iOS asset panel, see [Customize Asset Library for iOS](#broken-link-c9a4de). ### Insert Programmatically The Engine API inserts media without involving the UI. The pattern is the same for every media-backed block: 1. Create the block with `engine.block.create(_:)` — `.graphic` for images, videos, shapes, and stickers; `.audio` for audio. 2. For media-backed blocks, create a fill with `engine.block.createFill(_:)` and attach it with `engine.block.setFill(_:fill:)`. 3. Point the fill at a source file with `engine.block.setString(_:property:value:)` and the relevant property key (`fill/image/imageFileURI`, `fill/video/fileURI`). 4. Append the block to a parent — a page, a track, or another container — with `engine.block.appendChild(to:child:)`. 5. Configure layout and styling with the position, size, rotation, opacity, and z-order setters. Reach for the Engine API when you need reproducible output: a template applied to user data, a scene prepared before the editor opens, or a batch operation across many scenes. ## Referencing Existing Assets Inserted blocks point at media via URIs rather than embedding the bytes inline. The same URI can back any number of blocks — for example, place the same logo on every page of a brochure — and each instance keeps its own position, size, rotation, and opacity. When the same asset already lives in the asset library, look it up and reuse its URI rather than re-importing. The library entry stays a single record; each insertion is a separate block that references it. ## Media Lifecycle Within a Scene Once inserted, media participates in the regular scene lifecycle: - **Save.** `engine.scene.saveToString()` returns a scene definition that records each block's properties and the URIs it references. `engine.scene.saveToArchive()` returns a `Blob` that bundles the scene definition together with the actual media bytes. - **Reload.** `engine.scene.load(from:)` rebuilds the scene from a scene string and re-resolves any URIs the blocks point at. `engine.scene.load(from:)` unpacks an archive and rewires its internal references so the bundled media loads from the archive contents rather than the original URLs. - **Export.** `engine.block.export(_:mimeType:options:)` returns the rendered page as an image or PDF `Blob`. `engine.block.exportVideo(_:mimeType:options:)` returns an `AsyncThrowingStream` — iterate it to receive progress updates and the encoded video bytes when the stream finishes. The exporter resolves each block's fill at render time, so the output reflects each block's current properties at export. If a referenced URL stops resolving — the asset moves, the host disappears, the local file gets deleted — the affected block can't display the missing content until the URL is repaired or the media is re-inserted. Archives sidestep this by carrying the bytes inline. ## Embedding vs. Linking Media CE.SDK supports two strategies for how inserted media ends up in saved or exported output: | Mode | Description | Use Case | | -------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------- | | **Linked** (scene strings) | The output stores the asset's URI. The original file stays where it is. | Smaller files, shared assets, scenes that reuse hosted media. | | **Embedded** (archives) | The output bundles the asset bytes alongside the scene definition. | Offline editing, portable scenes, hand-off workflows. | `saveToString()` and `load(from:)` operate on the linked shape. `saveToArchive()` and `loadArchive(from:)` operate on the embedded shape. Pick the one that matches how the scene will be transported and reopened. ## Next Steps - [Insert Images](https://img.ly/docs/cesdk/mac-catalyst/insert-media/images-63848a/) — Add image fills to graphic blocks or insert independent image blocks. - [Insert Videos](https://img.ly/docs/cesdk/mac-catalyst/insert-media/videos-a5fa03/) — Add and trim a video block in a scene. - [Insert Audio](https://img.ly/docs/cesdk/mac-catalyst/insert-media/audio-c10f10/) — Add audio blocks for background music, voiceovers, and sound effects on the timeline. - [Insert Shapes or Stickers](https://img.ly/docs/cesdk/mac-catalyst/insert-media/shapes-or-stickers-20ac68/) — Add shapes and stickers as independent graphic blocks. - [Import Media Overview](https://img.ly/docs/cesdk/mac-catalyst/import-media/overview-84bb23/) — Bring local, remote, or device-sourced assets into CE.SDK so they're available to insert. - [Customize Asset Library for iOS](#broken-link-c9a4de) — Tailor the categories, sources, and ordering that show up in the iOS asset panel. --- ## 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: "Insert Shapes or Stickers" description: "Add shapes and stickers to your designs using CE.SDK. Create rectangles, ellipses, stars, polygons, lines, and custom vector paths programmatically." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/insert-media/shapes-or-stickers-20ac68/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/mac-catalyst/insert-media-a217f5/) > [Insert Shapes or Stickers](https://img.ly/docs/cesdk/mac-catalyst/insert-media/shapes-or-stickers-20ac68/) --- Add vector shapes and pre-made stickers to your designs programmatically with the Swift Engine API. Shapes require fills or strokes to be visible and offer type-specific properties like corner radius and star points. ![A 3x3 grid of shapes and stickers — rectangle, rounded rectangle, ellipse, star, hexagon, line, triangle, and two emoticon stickers.](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-insert-media-shapes-or-stickers) Shapes are vector graphics created with `engine.block.createShape(_:)` and attached to graphic blocks. CE.SDK supports six shape types: **rect**, **ellipse**, **star**, **polygon**, **line**, and **vectorPath**. Stickers are pre-made graphic assets loaded from sources like `ly.img.sticker`. > **Note:** For focused deep dives, see [Create Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-shapes-64acc0/) and [Create Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-stickers-cc46e5/). ```swift file=@cesdk_swift_examples/engine-guides-insert-media-shapes-or-stickers/InsertMediaShapesOrStickers.swift reference-only import Foundation import IMGLYEngine @MainActor func insertMediaShapesOrStickers(engine: Engine) async throws { // Demo scaffolding: a scene with an 800x600 page that hosts the demo grid. 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) // Demo scaffolding: shared block dimensions for the 3x3 grid the hero shows. let blockWidth: Float = 160 let blockHeight: Float = 140 // Demo scaffolding: resolve sample assets against the bundled asset base URL // and point `basePath` at it so the sticker source's relative references load. let baseURL = try engine.guidesBaseURL try engine.editor.setSettingString("basePath", value: baseURL.absoluteString) // Graphic blocks support shapes. let testBlock = try engine.block.create(.graphic) let supportsShape = try engine.block.supportsShape(testBlock) print("Graphic block supports shapes: \(supportsShape)") // Text blocks do not. let textBlock = try engine.block.create(.text) let textSupportsShape = try engine.block.supportsShape(textBlock) print("Text block supports shapes: \(textSupportsShape)") try engine.block.destroy(textBlock) try engine.block.destroy(testBlock) // Create a graphic block, attach a rect shape, then apply a solid color fill. let rectBlock = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(rectBlock, shape: rectShape) let rectFill = try engine.block.createFill(.color) try engine.block.setColor( rectFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.5, b: 0.9, a: 1.0), ) try engine.block.setFill(rectBlock, fill: rectFill) try engine.block.setWidth(rectBlock, value: blockWidth) try engine.block.setHeight(rectBlock, value: blockHeight) try engine.block.appendChild(to: page, child: rectBlock) // A rounded rectangle is a rect shape with non-zero corner radii. let roundedBlock = try engine.block.create(.graphic) let roundedShape = try engine.block.createShape(.rect) try engine.block.setShape(roundedBlock, shape: roundedShape) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusTL", value: 20) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusTR", value: 20) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusBL", value: 20) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusBR", value: 20) let roundedFill = try engine.block.createFill(.color) try engine.block.setColor( roundedFill, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.4, b: 0.2, a: 1.0), ) try engine.block.setFill(roundedBlock, fill: roundedFill) try engine.block.setWidth(roundedBlock, value: blockWidth) try engine.block.setHeight(roundedBlock, value: blockHeight) try engine.block.appendChild(to: page, child: roundedBlock) // An ellipse with equal width and height renders as a circle. let ellipseBlock = try engine.block.create(.graphic) let ellipseShape = try engine.block.createShape(.ellipse) try engine.block.setShape(ellipseBlock, shape: ellipseShape) let ellipseFill = try engine.block.createFill(.color) try engine.block.setColor( ellipseFill, property: "fill/color/value", color: .rgba(r: 0.3, g: 0.8, b: 0.4, a: 1.0), ) try engine.block.setFill(ellipseBlock, fill: ellipseFill) try engine.block.setWidth(ellipseBlock, value: blockWidth) try engine.block.setHeight(ellipseBlock, value: blockHeight) try engine.block.appendChild(to: page, child: ellipseBlock) // A 5-point star. `shape/star/innerDiameter` is normalized 0.0–1.0. let starBlock = try engine.block.create(.graphic) let starShape = try engine.block.createShape(.star) try engine.block.setShape(starBlock, shape: starShape) try engine.block.setInt(starShape, property: "shape/star/points", value: 5) try engine.block.setFloat(starShape, property: "shape/star/innerDiameter", value: 0.4) let starFill = try engine.block.createFill(.color) try engine.block.setColor( starFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.8, b: 0.0, a: 1.0), ) try engine.block.setFill(starBlock, fill: starFill) try engine.block.setWidth(starBlock, value: blockWidth) try engine.block.setHeight(starBlock, value: blockHeight) try engine.block.appendChild(to: page, child: starBlock) // A regular hexagon: 6 sides. let polygonBlock = try engine.block.create(.graphic) let polygonShape = try engine.block.createShape(.polygon) try engine.block.setShape(polygonBlock, shape: polygonShape) try engine.block.setInt(polygonShape, property: "shape/polygon/sides", value: 6) let polygonFill = try engine.block.createFill(.color) try engine.block.setColor( polygonFill, property: "fill/color/value", color: .rgba(r: 0.6, g: 0.2, b: 0.8, a: 1.0), ) try engine.block.setFill(polygonBlock, fill: polygonFill) try engine.block.setWidth(polygonBlock, value: blockWidth) try engine.block.setHeight(polygonBlock, value: blockHeight) try engine.block.appendChild(to: page, child: polygonBlock) // Attaching a line shape promotes the parent's fill into its stroke // automatically — `setStrokeEnabled(true)` here is explicit. let lineBlock = try engine.block.create(.graphic) let lineShape = try engine.block.createShape(.line) try engine.block.setShape(lineBlock, shape: lineShape) try engine.block.setStrokeEnabled(lineBlock, enabled: true) try engine.block.setStrokeWidth(lineBlock, width: 6) try engine.block.setStrokeColor( lineBlock, color: .rgba(r: 0.9, g: 0.2, b: 0.5, a: 1.0), ) try engine.block.setWidth(lineBlock, value: blockWidth) try engine.block.setHeight(lineBlock, value: 6) try engine.block.appendChild(to: page, child: lineBlock) // Custom shapes are defined by an SVG path. Coordinates scale with the block. let vectorPathBlock = try engine.block.create(.graphic) let vectorPathShape = try engine.block.createShape(.vectorPath) try engine.block.setShape(vectorPathBlock, shape: vectorPathShape) let trianglePath = "M 50,0 L 100,100 L 0,100 Z" try engine.block.setString( vectorPathShape, property: "shape/vector_path/path", value: trianglePath, ) let vectorPathFill = try engine.block.createFill(.color) try engine.block.setColor( vectorPathFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.7, b: 0.7, a: 1.0), ) try engine.block.setFill(vectorPathBlock, fill: vectorPathFill) try engine.block.setWidth(vectorPathBlock, value: blockWidth) try engine.block.setHeight(vectorPathBlock, value: blockHeight) try engine.block.appendChild(to: page, child: vectorPathBlock) let starProperties = try engine.block.findAllProperties(starShape) print("Star shape properties: \(starProperties)") // A sticker is a graphic block with a rect shape and an image fill. let stickerBlock = try engine.block.create(.graphic) let stickerShape = try engine.block.createShape(.rect) try engine.block.setShape(stickerBlock, shape: stickerShape) let stickerFill = try engine.block.createFill(.image) let stickerURL = baseURL.appendingPathComponent( "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_grin.svg", ) try engine.block.setURL(stickerFill, property: "fill/image/imageFileURI", value: stickerURL) try engine.block.setFill(stickerBlock, fill: stickerFill) // Preserve the sticker's aspect ratio inside the block bounds. if try engine.block.supportsContentFillMode(stickerBlock) { try engine.block.setContentFillMode(stickerBlock, mode: .contain) } // Tag the block as a sticker so the editor categorizes it correctly. try engine.block.setKind(stickerBlock, kind: "sticker") try engine.block.setWidth(stickerBlock, value: blockWidth) try engine.block.setHeight(stickerBlock, value: blockHeight) try engine.block.appendChild(to: page, child: stickerBlock) // Register the sticker asset source by loading its content.json. The // returned ID matches the `id` field in the JSON (here, `ly.img.sticker`). let stickerSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.sticker/content.json"), ) // Query the first page of stickers. `query` accepts a fuzzy search string; // `groups` narrows the result to a single category. let stickerResults = try await engine.asset.findAssets( sourceID: stickerSourceID, query: .init( query: nil, page: 0, groups: ["emoticons"], perPage: 5, ), ) print("Stickers in emoticons category: \(stickerResults.total)") // `apply(sourceID:assetResult:)` creates a graphic block from the asset's // metadata, attaches it to the current page, and returns its handle — there // is no need to call `appendChild` again. if let firstSticker = stickerResults.assets.first, let stickerFromLibrary = try await engine.asset.apply( sourceID: stickerSourceID, assetResult: firstSticker, ) { // The default content fill mode for an applied block is `.crop`. Switch to // `.contain` so the sticker preserves its aspect ratio inside the cell. if try engine.block.supportsContentFillMode(stickerFromLibrary) { try engine.block.setContentFillMode(stickerFromLibrary, mode: .contain) } try engine.block.setWidth(stickerFromLibrary, value: blockWidth) try engine.block.setHeight(stickerFromLibrary, value: blockHeight) } // Demo scaffolding: collect every block in creation order and place them in // the 3x3 grid the hero shows. let columns = 3 let spacingX: Float = 30 let spacingY: Float = 30 let gridStartX: Float = 130 let gridStartY: Float = 60 let shapeBlocks = [ rectBlock, roundedBlock, ellipseBlock, starBlock, polygonBlock, lineBlock, vectorPathBlock, ] let stickerBlocks = try engine.block.find(byKind: "sticker") let slots = shapeBlocks + stickerBlocks for (index, block) in slots.enumerated() { let col = index % columns let row = index / columns try engine.block.setPositionX(block, value: gridStartX + Float(col) * (blockWidth + spacingX)) try engine.block.setPositionY(block, value: gridStartY + Float(row) * (blockHeight + spacingY)) } try await engine.captureGuide(page, label: "hero") } ``` This guide covers creating each shape type, configuring shape-specific properties, applying fills, inserting stickers by hand, and querying stickers from the asset library. ## Using the Built-in UI ### Shapes Panel On iOS, the CE.SDK editor UI can expose shape entries when your editor configuration includes a shape source such as `ly.img.vector.shape`. Users select a shape in the asset library, then resize, rotate, position, and style the inserted block with the editor controls. ### Stickers Panel On iOS, sticker entries appear under their configured categories when the editor includes a sticker source such as `ly.img.sticker`. Users can browse the catalog and add a sticker to the canvas. The inserted item behaves like a graphic block and can be resized, rotated, aligned, and styled. ## Programmatic Shape Creation ### Check Shape Support Before attaching a shape to a block, confirm the block supports shapes with `supportsShape(_:)`. Graphic blocks return `true`; text blocks return `false`. ```swift highlight-checkShapeSupport // Graphic blocks support shapes. let testBlock = try engine.block.create(.graphic) let supportsShape = try engine.block.supportsShape(testBlock) print("Graphic block supports shapes: \(supportsShape)") // Text blocks do not. let textBlock = try engine.block.create(.text) let textSupportsShape = try engine.block.supportsShape(textBlock) print("Text block supports shapes: \(textSupportsShape)") try engine.block.destroy(textBlock) try engine.block.destroy(testBlock) ``` ### Create Rectangle Create rectangles with `createShape(.rect)` and attach them to a graphic block with `setShape(_:shape:)`. Apply a color fill to make the shape visible. ```swift highlight-createRectangle // Create a graphic block, attach a rect shape, then apply a solid color fill. let rectBlock = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(rectBlock, shape: rectShape) let rectFill = try engine.block.createFill(.color) try engine.block.setColor( rectFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.5, b: 0.9, a: 1.0), ) try engine.block.setFill(rectBlock, fill: rectFill) try engine.block.setWidth(rectBlock, value: blockWidth) try engine.block.setHeight(rectBlock, value: blockHeight) try engine.block.appendChild(to: page, child: rectBlock) ``` A bare graphic block has empty shape and fill placeholders on creation, so a shape and a fill are both required before the block renders. ### Create Rounded Rectangle Rectangles support per-corner radii. Set each corner individually with `setFloat(_:property:value:)` using `shape/rect/cornerRadiusTL`, `cornerRadiusTR`, `cornerRadiusBL`, and `cornerRadiusBR`. ```swift highlight-createRoundedRectangle // A rounded rectangle is a rect shape with non-zero corner radii. let roundedBlock = try engine.block.create(.graphic) let roundedShape = try engine.block.createShape(.rect) try engine.block.setShape(roundedBlock, shape: roundedShape) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusTL", value: 20) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusTR", value: 20) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusBL", value: 20) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusBR", value: 20) let roundedFill = try engine.block.createFill(.color) try engine.block.setColor( roundedFill, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.4, b: 0.2, a: 1.0), ) try engine.block.setFill(roundedBlock, fill: roundedFill) try engine.block.setWidth(roundedBlock, value: blockWidth) try engine.block.setHeight(roundedBlock, value: blockHeight) try engine.block.appendChild(to: page, child: roundedBlock) ``` Use different values per corner for asymmetric rounding, or the same value for a uniformly rounded rectangle. ### Create Ellipse Create circles and ovals with `createShape(.ellipse)`. The block's width and height determine the rendered shape: pass equal values for a circle, unequal values — like the 160×140 in the example below — for an oval. ```swift highlight-createEllipse // An ellipse with equal width and height renders as a circle. let ellipseBlock = try engine.block.create(.graphic) let ellipseShape = try engine.block.createShape(.ellipse) try engine.block.setShape(ellipseBlock, shape: ellipseShape) let ellipseFill = try engine.block.createFill(.color) try engine.block.setColor( ellipseFill, property: "fill/color/value", color: .rgba(r: 0.3, g: 0.8, b: 0.4, a: 1.0), ) try engine.block.setFill(ellipseBlock, fill: ellipseFill) try engine.block.setWidth(ellipseBlock, value: blockWidth) try engine.block.setHeight(ellipseBlock, value: blockHeight) try engine.block.appendChild(to: page, child: ellipseBlock) ``` ### Create Star Create stars with `createShape(.star)`. Configure the point count with `shape/star/points` and the inner diameter with `shape/star/innerDiameter`, which is normalized to `0.0`–`1.0`. Lower values produce thinner points. ```swift highlight-createStar // A 5-point star. `shape/star/innerDiameter` is normalized 0.0–1.0. let starBlock = try engine.block.create(.graphic) let starShape = try engine.block.createShape(.star) try engine.block.setShape(starBlock, shape: starShape) try engine.block.setInt(starShape, property: "shape/star/points", value: 5) try engine.block.setFloat(starShape, property: "shape/star/innerDiameter", value: 0.4) let starFill = try engine.block.createFill(.color) try engine.block.setColor( starFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.8, b: 0.0, a: 1.0), ) try engine.block.setFill(starBlock, fill: starFill) try engine.block.setWidth(starBlock, value: blockWidth) try engine.block.setHeight(starBlock, value: blockHeight) try engine.block.appendChild(to: page, child: starBlock) ``` ### Create Polygon Create regular polygons with `createShape(.polygon)`. Set the number of sides with `shape/polygon/sides` to render triangles (`3`), pentagons (`5`), hexagons (`6`), and beyond. ```swift highlight-createPolygon // A regular hexagon: 6 sides. let polygonBlock = try engine.block.create(.graphic) let polygonShape = try engine.block.createShape(.polygon) try engine.block.setShape(polygonBlock, shape: polygonShape) try engine.block.setInt(polygonShape, property: "shape/polygon/sides", value: 6) let polygonFill = try engine.block.createFill(.color) try engine.block.setColor( polygonFill, property: "fill/color/value", color: .rgba(r: 0.6, g: 0.2, b: 0.8, a: 1.0), ) try engine.block.setFill(polygonBlock, fill: polygonFill) try engine.block.setWidth(polygonBlock, value: blockWidth) try engine.block.setHeight(polygonBlock, value: blockHeight) try engine.block.appendChild(to: page, child: polygonBlock) ``` ### Create Line Create lines with `createShape(.line)`. Lines render through their stroke, so enable the stroke, set the color with `setStrokeColor(_:color:)` and the thickness with `setStrokeWidth(_:width:)`, then size the block so its height matches the stroke width — that's what makes the bounding box read as a line in the canvas. ```swift highlight-createLine // Attaching a line shape promotes the parent's fill into its stroke // automatically — `setStrokeEnabled(true)` here is explicit. let lineBlock = try engine.block.create(.graphic) let lineShape = try engine.block.createShape(.line) try engine.block.setShape(lineBlock, shape: lineShape) try engine.block.setStrokeEnabled(lineBlock, enabled: true) try engine.block.setStrokeWidth(lineBlock, width: 6) try engine.block.setStrokeColor( lineBlock, color: .rgba(r: 0.9, g: 0.2, b: 0.5, a: 1.0), ) try engine.block.setWidth(lineBlock, value: blockWidth) try engine.block.setHeight(lineBlock, value: 6) try engine.block.appendChild(to: page, child: lineBlock) ``` Attaching a line shape configures the parent block for line rendering: the engine disables the fill and enables the stroke for you. Calling `setStrokeEnabled(_:enabled:)` explicitly in the snippet keeps the intent visible. ### Create Vector Path Create custom shapes with `createShape(.vectorPath)`. Set the SVG path with `setString(_:property:value:)` using the `shape/vector_path/path` property. Path coordinates scale proportionally with the block's width and height. ```swift highlight-createVectorPath // Custom shapes are defined by an SVG path. Coordinates scale with the block. let vectorPathBlock = try engine.block.create(.graphic) let vectorPathShape = try engine.block.createShape(.vectorPath) try engine.block.setShape(vectorPathBlock, shape: vectorPathShape) let trianglePath = "M 50,0 L 100,100 L 0,100 Z" try engine.block.setString( vectorPathShape, property: "shape/vector_path/path", value: trianglePath, ) let vectorPathFill = try engine.block.createFill(.color) try engine.block.setColor( vectorPathFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.7, b: 0.7, a: 1.0), ) try engine.block.setFill(vectorPathBlock, fill: vectorPathFill) try engine.block.setWidth(vectorPathBlock, value: blockWidth) try engine.block.setHeight(vectorPathBlock, value: blockHeight) try engine.block.appendChild(to: page, child: vectorPathBlock) ``` The example draws a triangle by moving to the top center, then to the bottom-right and bottom-left corners. Any valid SVG path syntax works. ### Discover Shape Properties Use `findAllProperties(_:)` to list every configurable property for a given shape. This is useful when exploring an unfamiliar shape type or building generic editing tools. ```swift highlight-discoverShapeProperties let starProperties = try engine.block.findAllProperties(starShape) print("Star shape properties: \(starProperties)") ``` Each shape type exposes its own set of properties: - **Rectangle**: `shape/rect/cornerRadiusTL`, `cornerRadiusTR`, `cornerRadiusBL`, `cornerRadiusBR` - **Star**: `shape/star/points`, `shape/star/innerDiameter`, `shape/star/cornerRadius` - **Polygon**: `shape/polygon/sides` - **Vector Path**: `shape/vector_path/path` ## Programmatic Sticker Insertion ### Insert a Sticker A sticker is a graphic block with a rect shape and an image fill. Set the fill's `fill/image/imageFileURI` to point at the sticker source, then tag the block as a sticker with `setKind(_:kind:)` so the editor categorizes it correctly. ```swift highlight-stickerManualConstruction // A sticker is a graphic block with a rect shape and an image fill. let stickerBlock = try engine.block.create(.graphic) let stickerShape = try engine.block.createShape(.rect) try engine.block.setShape(stickerBlock, shape: stickerShape) let stickerFill = try engine.block.createFill(.image) let stickerURL = baseURL.appendingPathComponent( "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_grin.svg", ) try engine.block.setURL(stickerFill, property: "fill/image/imageFileURI", value: stickerURL) try engine.block.setFill(stickerBlock, fill: stickerFill) // Preserve the sticker's aspect ratio inside the block bounds. if try engine.block.supportsContentFillMode(stickerBlock) { try engine.block.setContentFillMode(stickerBlock, mode: .contain) } // Tag the block as a sticker so the editor categorizes it correctly. try engine.block.setKind(stickerBlock, kind: "sticker") try engine.block.setWidth(stickerBlock, value: blockWidth) try engine.block.setHeight(stickerBlock, value: blockHeight) try engine.block.appendChild(to: page, child: stickerBlock) ``` `setContentFillMode(_:mode:)` with `.contain` preserves the sticker's aspect ratio inside the block bounds. Guard the call with `supportsContentFillMode(_:)` since some block configurations don't expose the property. ### Query Stickers from Asset Library Register the sticker asset source with `addLocalAssetSourceFromJSON(_:)` pointed at the `ly.img.sticker` manifest. Once registered, browse the catalog with `findAssets(sourceID:query:)`. ```swift highlight-queryStickers // Register the sticker asset source by loading its content.json. The // returned ID matches the `id` field in the JSON (here, `ly.img.sticker`). let stickerSourceID = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent("ly.img.sticker/content.json"), ) // Query the first page of stickers. `query` accepts a fuzzy search string; // `groups` narrows the result to a single category. let stickerResults = try await engine.asset.findAssets( sourceID: stickerSourceID, query: .init( query: nil, page: 0, groups: ["emoticons"], perPage: 5, ), ) print("Stickers in emoticons category: \(stickerResults.total)") ``` The `query` parameter accepts a fuzzy search string; `groups` narrows the result to a single category. Pass `query: nil` to retrieve every sticker. For vector-based shape assets, use `ly.img.vector.shape` as the source ID instead. ### Apply a Sticker from the Library `apply(sourceID:assetResult:)` builds a graphic block from an asset's metadata — shape, image fill, `kind`, dimensions — and attaches it to the current page, so the manual `appendChild(to:child:)` step is unnecessary. The return value is an optional `DesignBlockID` that's `nil` when the asset source declines to materialize a block. ```swift highlight-applySticker // `apply(sourceID:assetResult:)` creates a graphic block from the asset's // metadata, attaches it to the current page, and returns its handle — there // is no need to call `appendChild` again. if let firstSticker = stickerResults.assets.first, let stickerFromLibrary = try await engine.asset.apply( sourceID: stickerSourceID, assetResult: firstSticker, ) { // The default content fill mode for an applied block is `.crop`. Switch to // `.contain` so the sticker preserves its aspect ratio inside the cell. if try engine.block.supportsContentFillMode(stickerFromLibrary) { try engine.block.setContentFillMode(stickerFromLibrary, mode: .contain) } try engine.block.setWidth(stickerFromLibrary, value: blockWidth) try engine.block.setHeight(stickerFromLibrary, value: blockHeight) } ``` The engine's default content fill mode for an applied sticker is `.crop`. Switch to `.contain` after `apply()` returns to preserve the sticker's aspect ratio — the snippet does this before the explicit `setWidth(_:value:)` and `setHeight(_:value:)` calls that resize the block into the demo grid. This is the recommended path when the sticker URL comes from the asset library. The manual construction path above is better when the sticker URL is known up front. ## Troubleshooting ### Shape Not Visible If a shape doesn't appear after creation: - **Verify a fill is applied.** Shapes without fills are invisible. Create a fill with `createFill(_:)` and apply it with `setFill(_:fill:)`. - **Check the block is added to the page.** Call `appendChild(to:child:)` to attach the block to the scene hierarchy. - **Ensure dimensions are set.** Call `setWidth(_:value:)` and `setHeight(_:value:)` to give the shape a size. ### Line Not Visible Lines render through their stroke, not a fill. Enable the stroke with `setStrokeEnabled(_:enabled:)` and set a stroke width — a line without an enabled stroke is invisible even when its color is set. ### Sticker Appears Cropped Set `setContentFillMode(_:mode:)` to `.contain` to preserve the sticker's aspect ratio inside the block bounds. Check `supportsContentFillMode(_:)` before calling, since some block configurations don't expose the property. ### Invalid Shape Type `createShape(_:)` accepts only the `ShapeType` enum cases: `.rect`, `.ellipse`, `.star`, `.polygon`, `.line`, `.vectorPath`. String-based overloads are deprecated; prefer the enum. ### No Assets Found Confirm that the expected source is registered and that its `content.json` URL is reachable. If you self-host the bundle, resolve the manifest and asset URLs against your own base URL. ### Asset Does Not Create a Block Treat the return value of `apply(sourceID:assetResult:)` as optional. A `nil` result means the source did not create a scene block for the selected asset. ## API Reference | Method | Description | | --- | --- | | `engine.block.create(_:)` | Create a graphic block to host a shape | | `engine.block.createShape(_:)` | Create a shape of the specified `ShapeType` | | `engine.block.supportsShape(_:)` | Check whether a block supports shapes | | `engine.block.setShape(_:shape:)` | Attach a shape to a graphic block | | `engine.block.findAllProperties(_:)` | List every configurable property of a block | | `engine.block.setInt(_:property:value:)` | Set an integer property (points, sides) | | `engine.block.setFloat(_:property:value:)` | Set a float property (corner radius, diameter) | | `engine.block.setString(_:property:value:)` | Set a string property such as a vector path | | `engine.block.setURL(_:property:value:)` | Set a URL property such as an image fill source | | `engine.block.createFill(_:)` | Create a fill of the specified `FillType` | | `engine.block.setFill(_:fill:)` | Apply a fill to a block | | `engine.block.setColor(_:property:color:)` | Set a color property | | `engine.block.setKind(_:kind:)` | Set a block's kind for categorization | | `engine.block.setContentFillMode(_:mode:)` | Set how a fill is laid out within the block | | `engine.block.supportsContentFillMode(_:)` | Check whether the block supports content fill modes | | `engine.block.setStrokeEnabled(_:enabled:)` | Enable or disable a block's stroke | | `engine.block.setStrokeWidth(_:width:)` | Set the stroke thickness | | `engine.block.setStrokeColor(_:color:)` | Set the stroke color | | `engine.block.setWidth(_:value:)` | Set the block width | | `engine.block.setHeight(_:value:)` | Set the block height | | `engine.block.appendChild(to:child:)` | Attach a block to a parent | | `engine.asset.addLocalAssetSourceFromJSON(_:)` | Register an asset source from a `content.json` URL | | `engine.asset.findAssets(sourceID:query:)` | Query assets from a registered source | | `engine.asset.apply(sourceID:assetResult:)` | Materialize an optional block from an asset result | ## Next Steps - [Colors](https://img.ly/docs/cesdk/mac-catalyst/colors-a9b79c/) — Work with colors, fills, and gradients - [Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects-6f88ac/) — Apply visual effects to design elements - [Position and Align](https://img.ly/docs/cesdk/mac-catalyst/create-composition/position-and-align-cc6b6a/) — Position elements precisely on the 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: "Insert Videos" description: "Insert videos into CE.SDK scenes programmatically using the Engine API for Swift." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/insert-media/videos-a5fa03/" --- > 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/) > [Insert Media Assets](https://img.ly/docs/cesdk/mac-catalyst/insert-media-a217f5/) > [Insert Videos](https://img.ly/docs/cesdk/mac-catalyst/insert-media/videos-a5fa03/) --- Add videos to your CE.SDK scenes programmatically with the Swift Engine API: build a graphic block, attach a video fill, and configure trim, position, and size from code. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-insert-media-videos) Videos in CE.SDK are graphic blocks with a video fill. A graphic block provides the shape and position on the page; the attached video fill carries the source URL and the trim metadata. ```swift file=@cesdk_swift_examples/engine-guides-insert-media-videos/InsertMediaVideos.swift reference-only import Foundation import IMGLYEngine @MainActor func insertMediaVideos(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: 30) 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 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) // Place an 800x450 frame at the center of the 1920x1080 page. try engine.block.setWidth(videoBlock, value: 800) try engine.block.setHeight(videoBlock, value: 450) try engine.block.setPositionX(videoBlock, value: 560) try engine.block.setPositionY(videoBlock, value: 315) try await engine.block.forceLoadAVResource(videoFill) let totalDuration = try engine.block.getAVResourceTotalDuration(videoFill) let trimOffset = 2.0 let trimLength = min(5.0, totalDuration - trimOffset) try engine.block.setTrimOffset(videoFill, offset: trimOffset) try engine.block.setTrimLength(videoFill, length: trimLength) try engine.block.setDuration(videoBlock, duration: trimLength) let graphicBlocks = try engine.block.find(byType: .graphic) for block in graphicBlocks { let fill = try engine.block.getFill(block) guard try engine.block.getType(fill) == FillType.video.rawValue else { continue } let uri = try engine.block.getString(fill, property: "fill/video/fileURI") let offset = try engine.block.getTrimOffset(fill) let length = try engine.block.getTrimLength(fill) print(String(format: "Video %u — trim %.2fs..+%.2fs, uri %@", block, offset, length, uri)) } try engine.block.destroy(videoBlock) } ``` This guide covers creating a video block, configuring trim, positioning the frame, finding existing videos in a scene, and removing them. ## Creating a Video Block Create a graphic block with `create(_:)`, attach a rectangular shape with `setShape(_:shape:)`, then create a video fill with `createFill(_:)` and point it at the source URL via `setString(_:property:value:)` using the `fill/video/fileURI` property. Append the graphic block to a page with `appendChild(to:child:)`. ```swift highlight-insertMediaVideos-createVideoBlock 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) ``` The source URI can be a remote URL or a local file path. The video fill is a separate block referenced by the graphic; configuration of trim and source URL happens on the fill, while position, size, and timeline placement happen on the graphic block. ## Positioning and Sizing Set the frame size with `setWidth(_:value:)` and `setHeight(_:value:)`, and the on-page position with `setPositionX(_:value:)` and `setPositionY(_:value:)`. All four values are expressed in the scene's design unit. ```swift highlight-insertMediaVideos-positionAndSize // Place an 800x450 frame at the center of the 1920x1080 page. try engine.block.setWidth(videoBlock, value: 800) try engine.block.setHeight(videoBlock, value: 450) try engine.block.setPositionX(videoBlock, value: 560) try engine.block.setPositionY(videoBlock, value: 315) ``` The origin sits at the top-left of the page (per the engine doc for `setPositionY`), so positive X moves the frame right and positive Y moves it down. ## Configuring Trim Control which portion of the source plays with `setTrimOffset(_:offset:)` and `setTrimLength(_:length:)`. Apply both to the **video fill** — the trim metadata lives on the fill, not on the graphic block that owns it. Call `forceLoadAVResource(_:)` first so the engine fetches the source and decodes its container, then read the source duration with `getAVResourceTotalDuration(_:)` to clamp trim values against the available content. ```swift highlight-insertMediaVideos-configureTrim try await engine.block.forceLoadAVResource(videoFill) let totalDuration = try engine.block.getAVResourceTotalDuration(videoFill) let trimOffset = 2.0 let trimLength = min(5.0, totalDuration - trimOffset) try engine.block.setTrimOffset(videoFill, offset: trimOffset) try engine.block.setTrimLength(videoFill, length: trimLength) try engine.block.setDuration(videoBlock, duration: trimLength) ``` The graphic block's timeline duration is independent of the fill's trim length — setting `setDuration(_:duration:)` on the block to match the trim length keeps the block visible on the timeline for exactly as long as its content plays. ## Finding Video Blocks Use `find(byType:)` with `DesignBlockType.graphic` to retrieve every graphic block in the scene, then filter by fill type. Graphic blocks can carry color, image, or video fills; reading the fill's type with `getType(_:)` and comparing against `FillType.video.rawValue` selects the video-backed ones. ```swift highlight-insertMediaVideos-findVideoBlocks let graphicBlocks = try engine.block.find(byType: .graphic) for block in graphicBlocks { let fill = try engine.block.getFill(block) guard try engine.block.getType(fill) == FillType.video.rawValue else { continue } let uri = try engine.block.getString(fill, property: "fill/video/fileURI") let offset = try engine.block.getTrimOffset(fill) let length = try engine.block.getTrimLength(fill) print(String(format: "Video %u — trim %.2fs..+%.2fs, uri %@", block, offset, length, uri)) } ``` For each block, read the source URI with `getString(_:property:)` and the trim properties with `getTrimOffset(_:)` and `getTrimLength(_:)`. ## Removing Videos Call `destroy(_:)` to remove a graphic block from the scene. Destroying the block detaches it from its parent page and releases the attached video fill. ```swift highlight-insertMediaVideos-removeVideo try engine.block.destroy(videoBlock) ``` ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `block.create(.graphic)` | Block | Create a new graphic block | | `block.createShape(.rect)` | Block | Create a rectangular shape | | `block.setShape(_:shape:)` | Block | Attach a shape to a graphic block | | `block.createFill(.video)` | Block | Create a video fill | | `block.setString(_:property:value:)` (`fill/video/fileURI`) | Block | Set the video source URL | | `block.getString(_:property:)` (`fill/video/fileURI`) | Block | Read the video source URL | | `block.setFill(_:fill:)` | Block | Attach a fill to a graphic block | | `block.getFill(_:)` | Block | Get the fill attached to a graphic block | | `block.getType(_:)` | Block | Read the engine type string | | `block.appendChild(to:child:)` | Block | Add a block to a parent | | `block.forceLoadAVResource(_:)` | Block | Load video metadata | | `block.getAVResourceTotalDuration(_:)` | Block | Read the source file's total duration in seconds | | `block.setTrimOffset(_:offset:)` | Block | Set the trim start point on the fill | | `block.getTrimOffset(_:)` | Block | Get the trim start point | | `block.setTrimLength(_:length:)` | Block | Set the trim duration on the fill | | `block.getTrimLength(_:)` | Block | Get the trim duration | | `block.setWidth(_:value:)` | Block | Set the block's width | | `block.setHeight(_:value:)` | Block | Set the block's height | | `block.setPositionX(_:value:)` | Block | Set the horizontal position | | `block.setPositionY(_:value:)` | Block | Set the vertical position | | `block.setDuration(_:duration:)` | Block | Set the block's timeline duration | | `block.find(byType:)` | Block | Find blocks by `DesignBlockType` | | `block.destroy(_:)` | Block | Remove a block | ## Next Steps - [Video Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/video-ec7f9f/) — Configure video fill properties beyond trim and source URL - [Create Videos Overview](https://img.ly/docs/cesdk/mac-catalyst/create-video/overview-b06512/) — Choose the right guide for creating and editing a video project - [Apply Filters and Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) — Enhance video appearance with adjustments and overlays - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Render scenes with video to MP4 or image 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: "Key Capabilities" description: "Explore CE.SDK’s key features—manual editing, automation, templates, AI tools, and full UI and API control." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/key-capabilities-dbb5b1/" --- > 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 Capabilities](https://img.ly/docs/cesdk/mac-catalyst/key-capabilities-dbb5b1/) --- This guide gives you a high-level look at what CreativeEditor SDK (CE.SDK) can do—and how deeply it can integrate into your workflows. Whether you’re building a design editor into your product, enabling automation, or scaling personalized content creation, CE.SDK provides a flexible and future-ready foundation. [Explore Demos](https://img.ly/showcases/cesdk/?tags=ios) It’s designed for developers, product teams, and technical decision-makers evaluating how CE.SDK fits their use case. - 100% client-side processing - Custom-built rendering engine for consistent cross-platform performance - Flexible enough for both low-code and fully custom implementations --- ## 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: "Key Concepts" description: "Explore CE.SDK’s key features—manual editing, automation, templates, AI tools, and full UI and API control." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/key-concepts-21a270/" --- > 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](https://img.ly/docs/cesdk/mac-catalyst/key-concepts-21a270/) --- CE.SDK is built on two distinct technical layers that work together seamlessly: - **User Interface** — Pre-built editors optimized for different use cases - **Engine Interface** — Core rendering and processing engine ![The different layers CE.SDK is made of, see description below.](layers.png) This intentional separation gives you powerful advantages: 1. **Cross-platform consistency** – The engine is cross-compiled to native web, iOS, Android, and Node.js, ensuring identical output everywhere 2. **Custom UI** – Build your own UI for simpler tools and workflows 3. **Headless automation** – Run the engine independently for automations and batch processing, both client-side and server-side ## Creative Engine The Creative Engine powers all core editing operations. It handles rendering, processing, and manipulation across images, layouts, text, video, audio, and vectors. **What the Engine Does:** - Maintains the scene file (your structured content) - Renders the canvas in real-time - Handles block positioning and resizing - Applies filters and effects to images - Manages text editing and typography - Controls templates with role-based permissions - Displays smart guides and snap lines Every engine capability is exposed through a comprehensive API, letting you build custom UIs, workflows, and automations. ## Headless / Engine only Use the engine without any UI for powerful automation scenarios: **Client-side automation** Perfect for in-browser batch operations and dynamic content generation without server dependencies. **Server-side automation with Node.js** Use the [Node.JS SDK](https://img.ly/docs/cesdk/mac-catalyst/what-is-cesdk-2e7acd/) for following scenarios: - **High-resolution processing** – Edit on the client with preview quality, then render server-side with full-resolution assets - **Bulk generation** – Create a large volume of design variations for variable data printing - **Non-blocking workflows** – Let users continue designing while exports process in the background **Server-side export with the CE.SDK Renderer** When exporting complex graphics and videos, the [CE.SDK Renderer](#broken-link-7f3e9a) can make use of GPU acceleration and video codecs on Linux server environments. **Plugin development** When building CE.SDK plugins, you get direct API access to manipulate canvas elements programmatically. ## User Interface Components CE.SDK includes pre-built UI configurations optimized for different use cases: - **Photo editing** — Advanced image editing tools and filters - **Video editing** — Timeline-based video editing and effects - **Design editing** — Layout and graphic design tools (similar to Canva) - **2D product design** — Apparel, postcards, and custom product templates More configurations are coming based on customer needs. ## UI Customization While UI configurations provide a solid foundation, you maintain control over the user experience: - Apply **custom color schemes** and branding to match your product - Add **custom asset libraries** with your own fonts, images, graphics, videos, and audio The plugin architecture lets you add custom buttons and panels throughout the interface, ensuring the editor feels native to your product. --- ## 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: "Licensing" description: "Understand CE.SDK’s flexible licensing, trial options, and how keys work across dev, staging, and production." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/licensing-8aa063/" --- > 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/) > [Licensing](https://img.ly/docs/cesdk/mac-catalyst/licensing-8aa063/) --- Thanks for your interest in CreativeEditor SDK (CE.SDK). We offer flexible commercial licensing options to support teams and projects of all sizes. Whether you're building a new product or scaling an existing one, our goal is to provide the best creative editing experience—backed by a licensing model that aligns with your needs. Get in touch with us through our [contact sales form](https://img.ly/forms/contact-sales). ## Commercial Licensing CE.SDK is offered through a subscription-based commercial model. This allows us to: - Deliver ongoing updates and performance improvements - Ensure compatibility with new browsers and devices - Provide dedicated technical support - Build long-term partnerships with our customers ## How Licensing Works CE.SDK licenses are tied to a single commercial product instance, verified by the hostname for web apps and bundle/app ID for mobile apps. Licensing typically uses remote validation and includes lightweight event tracking. It’s possible to disable tracking or use offline-compatible options. To explore these options, [contact our sales team](https://img.ly/forms/contact-sales). For details on which operations count as an export, see [Export Counting](https://img.ly/docs/cesdk/mac-catalyst/export-counting-613923/). ## Trial License Key Trial licenses are available for evaluation and testing and are valid for **30 days**. They provide full access to CE.SDK’s features so you can explore its capabilities in your environment. If you need more time to evaluate, [contact our sales team](https://img.ly/forms/contact-sales). ## Testing and Production Paid license keys can be used across development, staging, and production environments. Multiple domains or app identifiers can be added to support this setup. --- ## 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: "LLMs.txt" description: "Our documentation is available in LLMs.txt format" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/llms-txt-eb9cc5/" --- > 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:** [Build with AI](https://img.ly/docs/cesdk/mac-catalyst/get-started/build-with-ai-k7m9p2/) > [LLMs.txt](https://img.ly/docs/cesdk/mac-catalyst/llms-txt-eb9cc5/) --- > **Note:** You can also connect your AI assistant directly to our documentation using our > [MCP Server](https://img.ly/docs/cesdk/mac-catalyst/get-started/mcp-server-fde71c/). This enables real-time search and > retrieval without downloading large files. Our documentation is now available in LLMs.txt format, optimized for AI reasoning engines. To better support platform-specific development, we've created separate documentation files for each platform. For developers, this means you can now access documentation tailored to your specific platform, whether it's iOS, Android, Web, or any other supported platform. This approach allows for a more focused and efficient use of AI tools in your development workflow. [Download](getFullUrl\(`/$\{props.platform.slug}/llms-full.txt`\)) These documentation files are substantial in size, with token counts exceeding the context windows of many AI models. This guide explains how to download and effectively use these platform-specific documentation files with AI tools to accelerate your development process. ## What are LLMs.txt files? LLMs.txt is an emerging standard for making documentation AI-friendly. Unlike traditional documentation formats, LLMs.txt: - Presents content in a clean, markdown-based format - Eliminates extraneous HTML, CSS, and JavaScript - Optimizes content for AI context windows - Provides a comprehensive view of documentation in a single file By using our platform-specific LLMs.txt files, you'll ensure that AI tools have the most relevant and complete context for helping with your development tasks. ## Markdown Content Negotiation Our documentation pages also serve clean markdown directly when requested with the `Accept: text/markdown` HTTP header. AI agents and tools that support content negotiation can fetch any documentation page and receive a markdown response instead of HTML — no separate download required. ```bash curl -H "Accept: text/markdown" https://img.ly/docs/cesdk/react/get-started/overview/ ``` This means AI tools like web-browsing agents can access individual pages in a format optimized for their context windows without needing the full LLMs.txt bundle. ## Handling Large Documentation Files Due to the size of our documentation files (upward of 500 000 tokens) most AI tools will face context window limitations. Standard models typically have context windows ranging from 8,000 to 200,000 tokens, making it challenging to process our complete documentation in a single session. ### Using Large Documentation Files To work with our complete documentation files, use an AI model with a large context window. Many current models support 200,000+ tokens, and some support over 1 million tokens. Check your model's context window limits when loading the full documentation file. --- ## 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: "Open the Editor" description: "Learn how to load and create scenes, set the zoom level, and configure URI resolvers." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/overview-99444b/) - Learn how to load and create scenes, set the zoom level, and configure URI resolvers. - [Load a Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/) - Load existing design scenes into the editor to resume or modify previous work. - [Start With Blank Canvas](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/blank-canvas-18ff05/) - Create a new scene from scratch with the CE.SDK Engine for Swift, then size the page and add your first design element. - [Create From Image](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-image-ad9b5e/) - Open the editor using an image as the base design, with tools ready for immediate editing. - [Create From Video](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-video-86beb0/) - Load a video file into the editor to start editing frame-based or time-based video content. - [Create From Template](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-template-46c096/) - Start the editor with a pre-designed template for faster editing and consistent output. - [Import a Design](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design-73b9c5/) - Load saved CE.SDK scenes from a URL or string, load self-contained archives, and create editable scenes from images and videos with the Swift Engine API. - [Set Zoom Level](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/set-zoom-level-d31896/) - Control the canvas zoom level with the CE.SDK Engine — set and read zoom, frame blocks, follow content with auto-fit, clamp the camera, and observe zoom changes. - [URI Resolver](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/uri-resolver-36b624/) - Intercept and transform asset URIs before CE.SDK loads them — for authentication, redirects, and custom resolution logic. --- ## 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: "Start With Blank Canvas" description: "Create a new scene from scratch with the CE.SDK Engine for Swift, then size the page and add your first design element." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/blank-canvas-18ff05/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [Start With Blank Canvas](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/blank-canvas-18ff05/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-scene-from-scratch/CreateSceneFromScratch.swift reference-only import Foundation import IMGLYEngine @MainActor func createSceneFromScratch(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 pageFill = try engine.block.createFill(.color) try engine.block.setColor(pageFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.95, b: 0.96, a: 1)) try engine.block.setFill(page, fill: pageFill) let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.star)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: .rgba(r: 0.27, g: 0.52, b: 0.96, 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.setPositionX(block, value: 250) try engine.block.setPositionY(block, value: 150) try engine.block.appendChild(to: page, child: block) try engine.scene.enableZoomAutoFit( page, axis: .both, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) } ``` Create a new scene from scratch to build designs with complete control over canvas dimensions and initial content. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-create-scene-from-scratch) Starting from a blank canvas lets you build new designs without pre-existing content. `engine.scene.create()` creates an empty scene with its own camera, ready for pages and blocks. This differs from loading a template or an image, which start with existing content. See [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) for more on scene hierarchy. > **Other Ways to Create Scenes:** You can also start with existing content:* [Create From Image](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-image-ad9b5e/) — Start with an image as the base > * [Load a Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/) — Resume editing a previously saved design ## Create an Empty Scene Call `engine.scene.create(sceneLayout:)` to create a new design scene with a camera attached. The scene itself has no dimensions — you set the canvas size on each page, shown next. ```swift highlight-createSceneFromScratch-create let scene = try engine.scene.create() ``` The `sceneLayout` parameter controls how pages are arranged: `.free` for independent positioning, `.verticalStack` or `.horizontalStack` for aligned layouts, and `.depthStack` for layered compositions. It defaults to `.free`. ## Configure Page Size Create a page with `engine.block.create(.page)`, set its dimensions with `setWidth(_:value:)` and `setHeight(_:value:)` in design units, then parent it to the scene with `appendChild(to:child:)`. ```swift highlight-createSceneFromScratch-add-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) ``` Width and height are separate values rather than a single size object. Repeat these calls to add as many pages as your design needs. ## Set a Background Color Give the page a solid background by assigning it a color fill. Create a `.color` fill, set its `"fill/color/value"` property, then assign it to the page with `setFill(_:fill:)`. ```swift highlight-createSceneFromScratch-background let pageFill = try engine.block.createFill(.color) try engine.block.setColor(pageFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.95, b: 0.96, a: 1)) try engine.block.setFill(page, fill: pageFill) ``` Colors use components from `0` to `1`. The same `Color` value also accepts `.cmyk(...)` for print workflows and `.spot(...)` for named brand colors. ## Add Your First Block Create a graphic block, assign it a shape and a fill so it has a visual representation, size and position it, then append it to the page. A graphic block needs both a shape and a fill to render. ```swift highlight-createSceneFromScratch-add-block let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.star)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: .rgba(r: 0.27, g: 0.52, b: 0.96, 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.setPositionX(block, value: 250) try engine.block.setPositionY(block, value: 150) try engine.block.appendChild(to: page, child: block) ``` `createShape(_:)` accepts shapes such as `.star`, `.rect`, and `.ellipse`; `createFill(_:)` accepts fills such as `.color`, `.image`, and the gradient types. ## Enable Auto-Fit Zoom For interactive editing, enable auto-fit zoom so the page stays framed when the viewport resizes. ```swift highlight-createSceneFromScratch-zoom try engine.scene.enableZoomAutoFit( page, axis: .both, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) ``` `enableZoomAutoFit(_:axis:)` continuously adjusts the zoom level to fit a block. Use `.horizontal` to fit the width, `.vertical` to fit the height, or `.both` to fit both; the padding parameters add space around the content. Only one block per scene can use auto-fit at a time, and it has no effect while the editor UI controls the zoom level. For a one-time adjustment, use `zoom(to:)`, and call `disableZoomAutoFit(_:)` to stop the continuous fit. ## API Reference | Method | Description | | --- | --- | | `engine.scene.create(sceneLayout:)` | Create a new empty scene with a camera | | `engine.block.create(_:)` | Create a block such as `.page` or `.graphic` | | `engine.block.setWidth(_:value:)` / `setHeight(_:value:)` | Set a block's dimensions in design units | | `engine.block.setPositionX(_:value:)` / `setPositionY(_:value:)` | Position a block on its parent | | `engine.block.appendChild(to:child:)` | Add a block as a child of another | | `engine.block.createShape(_:)` / `setShape(_:shape:)` | Create and assign a shape | | `engine.block.createFill(_:)` / `setFill(_:fill:)` | Create and assign a fill | | `engine.block.setColor(_:property:color:)` | Set a color property such as `"fill/color/value"` | | `engine.scene.enableZoomAutoFit(_:axis:)` | Continuously fit a block in the viewport | | `engine.scene.zoom(to:)` | Frame a block once | | `engine.scene.disableZoomAutoFit(_:)` | Stop auto-fit zoom | ## Next Steps - [Save](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) — Persist your design to a file or backend service - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Learn about scene hierarchy and block relationships - [Create From Image](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-image-ad9b5e/) — Start with an existing image instead of a blank 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: "Create From Image" description: "Open the editor using an image as the base design, with tools ready for immediate editing." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-image-ad9b5e/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [Create From Image](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-image-ad9b5e/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-scene-from-image-url/CreateSceneFromImageURL.swift reference-only import Foundation import IMGLYEngine @MainActor func createSceneFromImageURL(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg") try await engine.scene.create(fromImage: imageURL) guard let page = try engine.block.find(byType: .page).first else { return } let pageFill = try engine.block.getFill(page) let isImageFill = try engine.block.getType(pageFill) == FillType.image.rawValue print("Page is filled with an image: \(isImageFill)") // The image loaded as the page's content — captured as the guide's hero. try await engine.captureGuide(page, label: "hero") } ``` ```swift file=@cesdk_swift_examples/engine-guides-create-scene-from-image-blob/CreateSceneFromImageBlob.swift reference-only import Foundation import IMGLYEngine @MainActor func createSceneFromImageBlob(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg") let blob = try await URLSession.shared.data(from: imageURL).0 let url = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("jpg") try blob.write(to: url, options: .atomic) try await engine.scene.create(fromImage: url) } ``` Create an editable scene from an image with the Swift Engine API. The engine builds a single-page scene sized to the image and configured in pixel design units, ready for immediate editing. ![A photograph loaded as an editable scene, its page sized to and filled by the source image.](./assets/swift-based.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-create-scene-from-image-url) `engine.scene.create(fromImage:)` fetches the image, creates a scene whose page matches the image's dimensions, and adds the image directly as the page's fill. This is the starting point for image-editing workflows where users enhance, annotate, or transform an existing image. ## Create a Scene from an Image URL Pass the image source to `create(fromImage:)`. The source is a `URL` — either a local file or a remote address. ```swift highlight-createFromImage-url try await engine.scene.create(fromImage: imageURL) ``` The scene's page dimensions match the image, and the scene is configured in pixel design units. ### Inspect the Page Fill The image becomes the page's fill rather than a separate image block. Locate the page with `find(byType:)` to reach that fill: ```swift highlight-findByType-url guard let page = try engine.block.find(byType: .page).first else { return } ``` Read the page's fill and confirm it is an image fill by comparing its type against `FillType.image`: ```swift highlight-check-fill-url let pageFill = try engine.block.getFill(page) let isImageFill = try engine.block.getType(pageFill) == FillType.image.rawValue print("Page is filled with an image: \(isImageFill)") ``` ## Create a Scene from a Blob When the image arrives as raw `Data` — from a file picker, a network response, or any other source — write it to a temporary file and create the scene from that file's URL. First, get the image data. This example fetches it to stand in for data your app already holds. ```swift highlight-blob-swift let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg") let blob = try await URLSession.shared.data(from: imageURL).0 ``` Write the data to a temporary file and keep its URL. ```swift highlight-objectURL-swift let url = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("jpg") try blob.write(to: url, options: .atomic) ``` Use that URL as the source for the scene. ```swift highlight-initialImageURL-swift try await engine.scene.create(fromImage: url) ``` As with a remote URL, the page dimensions match the image and the scene uses pixel design units. ## Configure Scene Parameters `create(fromImage:dpi:pixelScaleFactor:sceneLayout:)` accepts optional parameters that control how the image maps to scene dimensions and how pages are arranged. | Parameter | Default | Description | | --- | --- | --- | | `dpi` | `300` | Dots per inch of the scene, which sets the relationship between pixel and physical dimensions. | | `pixelScaleFactor` | `1` | The display's pixel scale factor, used to account for high-resolution screens. | | `sceneLayout` | `.free` | Page arrangement: `.free`, `.horizontalStack`, `.verticalStack`, or `.depthStack`. | To later save your scene, see [Saving Scenes](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/). ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.create(fromImage:dpi:pixelScaleFactor:sceneLayout:)` | Create a scene whose single page is sized to the image and filled with it | | `engine.block.find(byType:)` | Find all blocks of a `DesignBlockType`, such as the page | | `engine.block.getFill(_:)` | Get the fill block attached to a block | | `engine.block.getType(_:)` | Read a block's type string, including the fill type | ## Next Steps - [Saving Scenes](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) — Persist the edited scene to a string or an archive. - [Load Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/) — Open the editor from a previously saved scene file. - [Create From Video](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-video-86beb0/) — Start the editor from a video instead of an image. - [Blank Canvas](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/blank-canvas-18ff05/) — Launch the editor with an empty 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: "Create From Template" description: "Start the editor with a pre-designed template for faster editing and consistent output." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-template-46c096/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [Create From Template](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-template-46c096/) --- ```swift file=@cesdk_swift_examples/engine-guides-from-template/FromTemplate.swift reference-only import Foundation import IMGLYEngine @MainActor func fromTemplate(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 templateString = try await engine.scene.saveToString() try await engine.scene.load(from: templateString) try await engine.scene.applyTemplate(from: templateURL) if let firstTextBlock = try engine.block.find(byType: .text).first { try engine.block.replaceText(firstTextBlock, text: "Your Company") } } ``` Load pre-designed templates to give users a professional starting point instead of a blank canvas. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-from-template) Templates provide consistent layouts and styling that users can customize for their own needs. CE.SDK loads templates from remote or local scene URLs, from serialized strings, and applies template content to an existing scene while preserving its page dimensions. ## Load a Template from URL The most common approach is loading a template from a `.scene` file URL. Point `load(from:)` at the URL of your template, and the engine replaces the current scene with the loaded template. ```swift highlight-fromTemplate-loadFromURL try await engine.scene.load(from: templateURL) ``` The scene file references its assets by URL, so those assets must stay reachable at their original locations. For a fully self-contained template, use an archive instead — see [Load a Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/). ## Load a Template from String When a template is stored as a serialized string — in a database or local storage — load it with the same `load(from:)` method. The string is the scene content produced by `engine.scene.saveToString()`. ```swift highlight-fromTemplate-loadFromString let templateString = try await engine.scene.saveToString() try await engine.scene.load(from: templateString) ``` This is useful for restoring saved user designs or serving templates from your backend. ## Apply a Template to an Existing Scene To populate an existing scene with template content while keeping its current page dimensions, use `applyTemplate(from:)`. The template's content is automatically adjusted to fit the existing page size and design unit. ```swift highlight-fromTemplate-applyTemplate try await engine.scene.applyTemplate(from: templateURL) ``` Use this when the canvas size is already set — for a fixed output format, for example — and you want to drop in template content without changing those dimensions. ## Modify Template Content After loading a template, customize its content with the block APIs. Find the elements you want to change and update them. ```swift highlight-fromTemplate-modifyContent if let firstTextBlock = try engine.block.find(byType: .text).first { try engine.block.replaceText(firstTextBlock, text: "Your Company") } ``` Common modifications include: - **Replacing text**: `engine.block.replaceText(_:text:)` swaps the content of a text block. - **Swapping images**: set `fill/image/imageFileURI` on a graphic block's image fill — see [Image Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/image-e9cb5c/). - **Adjusting colors**: set `fill/color/value` on a block's fill — see [Color Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/color-7129cd/). ## Troubleshooting **Template fails to load** - Verify the URL is reachable and returns a valid `.scene` file. - Ensure the template format is compatible with your CE.SDK version. **Assets not displaying after load** - Scene files store asset references as URLs; ensure those URLs remain reachable. - Use an archive (`.zip`) for a self-contained template with bundled assets. - Configure a [URI resolver](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/uri-resolver-36b624/) if assets are hosted on a different server. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.load(from: URL)` | Load a scene from a remote or local URL | | `engine.scene.load(from: String)` | Load a scene from a serialized string | | `engine.scene.applyTemplate(from: URL)` | Apply a template to the current scene from a URL | | `engine.scene.applyTemplate(from: String)` | Apply a template to the current scene from a string | | `engine.scene.saveToString()` | Serialize the current scene to a string | | `engine.block.find(byType:)` | Find all blocks of a given type | | `engine.block.replaceText(_:text:)` | Replace the text content of a text block | ## Next Steps - [Load a Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/) — Load saved scenes from various sources - [Save a Design](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) — Save your customized template - [Import a Design](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design-73b9c5/) — Load previously saved scenes, self-contained archives, or create editable scenes from images and videos. --- ## 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 Video" description: "Load a video file into the editor to start editing frame-based or time-based video content." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-video-86beb0/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [Create From Video](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-video-86beb0/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-scene-from-video-url/CreateSceneFromVideoURL.swift reference-only import Foundation import IMGLYEngine @MainActor func createSceneFromVideoURL(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 block = try engine.block.find(byType: .graphic).first else { return } try engine.block.setOpacity(block, value: 0.5) } ``` Open CE.SDK with a video as the starting point for editing. The scene's page dimensions match the video resolution and the scene is set up for time-based content. > **Reading time:** 3 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-create-scene-from-video-url) Starting from an existing video lets you build editors for customizing video content — trimmers, overlay editors, or upload-and-edit flows. Create a scene from a single video with `engine.scene.create(fromVideo:)`. This guide covers creating a scene from a video and reaching the video block to adjust its properties. ## Create a Scene From a Video URL Pass a video URL to `engine.scene.create(fromVideo:)`. The URL can point to a local file or a remote resource. The call loads the video and returns a handle to the new scene. ```swift highlight-createSceneFromVideoURL-createFromVideo try await engine.scene.create(fromVideo: videoURL) ``` When you start from a video, the scene's page dimensions match the resource, the scene uses pixel design units, and it is set up for time-based editing. ## Work With the Video Block CE.SDK places the video inside a graphic block that carries a video fill. Retrieve it with `engine.block.find(byType:)`, which returns every block of a given `DesignBlockType`. A scene created from a video contains a single graphic block, so the first result is the video block. From there, modify the block like any other element — for example, change its opacity with `engine.block.setOpacity(_:value:)`. ```swift highlight-createSceneFromVideoURL-workWithBlock guard let block = try engine.block.find(byType: .graphic).first else { return } try engine.block.setOpacity(block, value: 0.5) ``` See [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) for the full Block API. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.create(fromVideo:)` | Create a scene from a video URL, matching the scene dimensions to the video | | `engine.block.find(byType:)` | Find all blocks of a `DesignBlockType` | | `engine.block.setOpacity(_:value:)` | Set a block's opacity | ## Next Steps - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Edit blocks, layout, and properties in the scene - [Saving Scenes](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) — Persist your scene and reload it later - [Insert Videos](https://img.ly/docs/cesdk/mac-catalyst/insert-media/videos-a5fa03/) — Add and configure additional video blocks programmatically - [Control Audio and Video](https://img.ly/docs/cesdk/mac-catalyst/create-video/control-daba54/) — Trim, seek, and control video and audio playback --- ## 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 a Design" description: "Load saved CE.SDK scenes from a URL or string, load self-contained archives, and create editable scenes from images and videos with the Swift Engine API." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design-73b9c5/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [Import a Design](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design-73b9c5/) --- ```swift file=@cesdk_swift_examples/engine-guides-import-design/ImportDesign.swift reference-only import Foundation import IMGLYEngine @MainActor func importDesign(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) // Produce a serialized scene string for the next section. In production it // comes from your own storage — a database row, a file on disk, or the result // of a previous saveToString() call. let sceneString = try await engine.scene.saveToString() try await engine.scene.load(from: sceneString) // Produce a self-contained 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 — created earlier with saveToArchive(). Archives // use the .imgly extension now (.zip remains loadable). let archiveData = try await engine.scene.saveToArchive() let archiveURL = FileManager.default.temporaryDirectory .appendingPathComponent("imported-design-\(UUID().uuidString).imgly") try archiveData.write(to: archiveURL) try await engine.scene.load(from: archiveURL) let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg") try await engine.scene.create(fromImage: imageURL) 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) if let text = try engine.block.find(byType: .text).first { try engine.block.replaceText(text, text: "Updated heading") } } ``` Open existing designs in CE.SDK: load previously saved scenes from a URL or string, load self-contained archives that bundle their own assets, and create editable scenes directly from images and videos. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-import-design) CE.SDK supports several ways to open a design beyond starting from a blank canvas. Each `load` or `create` call replaces the active scene, so the imported design becomes the one your app edits and exports. ## Understanding Import Methods CE.SDK provides three approaches for importing a design, each suited to a different source: - **Scene files** store the design structure, layout, and properties, referencing assets such as images and fonts by their URLs. They are lightweight but depend on those asset URLs staying reachable. - **Archives** bundle the scene file together with accessible referenced assets and use relative references. They are larger but self-contained and portable across environments — see the dedicated [Import Design from Archive](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-archive-dde9fa/) guide for the full workflow. - **Media-based scenes** build an editable design directly from a source image or video. Scene files and archives both use the `.imgly` extension — `.scene` and `.zip` files also load — and the same `load(from:)` call opens either kind. ## Load Saved CE.SDK Scenes Load a previously saved scene to resume editing. CE.SDK offers three entry points depending on where the saved scene lives. ### From a URL Use `load(from:)` with a `URL` to load a scene stored on a server or in cloud storage. This fits cloud-based editing where users open designs from any device. ```swift highlight-importDesign-loadFromURL let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: sceneURL) ``` The engine fetches the scene file asynchronously and replaces the current scene. All asset URLs referenced inside the scene must stay reachable for it to render correctly. ### From a String Use `load(from:)` with a `String` when you already hold the scene content in memory — for example a value read from a database or a file on disk, or the result of a previous `saveToString()` call. Pass that serialized string to `load(from:)`: ```swift highlight-importDesign-loadFromString try await engine.scene.load(from: sceneString) ``` ### From an Archive An archive bundles the scene with all of its assets, so it loads even when the original asset URLs are no longer reachable. Pass the archive's location — a remote archive on your CDN or a local file URL, created earlier with `saveToArchive()` — to the same `load(from:)` call; the engine detects the file kind automatically: ```swift highlight-importDesign-loadFromArchive try await engine.scene.load(from: archiveURL) ``` ## Create Scenes from Media Build an editable scene directly from a source image or video instead of loading a saved design. ### From an Image Use `create(fromImage:)` to build a single-page design scene around an image. Pass `dpi:` (default `300`) to control how the image's pixels map to the scene's design units. ```swift highlight-importDesign-createFromImage let imageURL = baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg") try await engine.scene.create(fromImage: imageURL) ``` The scene is ready for editing — add text, shapes, and effects on top of the image. ### From a Video Use `create(fromVideo:)` to build a video scene with the video as the page content, set up for timeline-based editing. ```swift highlight-importDesign-createFromVideo 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) ``` ## Choosing the Right Import Method Pick the method that matches your source and constraints: - **Resuming saved work?** Use `load(from:)` with the URL or string of a scene you previously saved. - **Assets might be unavailable?** Use `load(from:)` with an archive for a self-contained scene with bundled assets. - **Starting from media?** Use `create(fromImage:)` or `create(fromVideo:)` to build an editable scene from a source file. - **Need portability?** Save and load archives that bundle everything together. - **Want lightweight saves?** Use scene files when assets stay reachable at their URLs. ## Asset Availability Considerations When you load a scene file rather than an archive, the referenced assets must stay reachable at their original URLs. Scene files store those references as URLs, so an image saved at `https://example.com/image.jpg` must still be served there when the scene loads. Archives avoid this by bundling assets inside the archive file and using relative references, which makes them portable across environments. See [Import Design from Archive](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-archive-dde9fa/) for the full archive workflow. ## Working with Loaded Scenes After importing, the design becomes the active scene. Query and modify it immediately with the block APIs — for example, if it contains a text block, replace that block's content: ```swift highlight-importDesign-modifyScene if let text = try engine.block.find(byType: .text).first { try engine.block.replaceText(text, text: "Updated heading") } ``` ## Troubleshooting **Scene loads with missing images or fonts** - Confirm every asset URL referenced in the scene is still reachable. - Use an archive instead of a scene file when assets might move or become unavailable. **Archive fails to load** - Ensure the archive was created with `saveToArchive()` and the file isn't corrupted. - Confirm the archive URL is reachable and the file isn't truncated. **Image or video fails to load** - Confirm the media URL is reachable and returns the file. - Confirm the media is in a supported image or video format. ## API Reference | Method | Description | | --- | --- | | `engine.scene.load(from url: URL)` | Load a scene or archive from a remote or local URL (file kind detected automatically) | | `engine.scene.load(from string: String)` | Load a scene from serialized scene content | | `engine.scene.loadArchive(from url: URL)` | Load a scene archive from a URL | | `engine.scene.saveToString()` | Serialize the active scene to a string for later loading | | `engine.scene.saveToArchive()` | Save the active scene and its assets as an archive (persist it with the `.imgly` extension) | | `engine.scene.create(fromImage url: URL, dpi:)` | Create an editable design scene from an image | | `engine.scene.create(fromVideo url: URL)` | Create a video scene from a video | | `engine.block.find(byType:)` | Find all blocks of a `DesignBlockType` in the active scene | | `engine.block.replaceText(_:text:)` | Replace the text content of a text block | ## Next Steps - [Import Design from Archive](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-archive-dde9fa/) — Work with self-contained `.imgly` archives that bundle every referenced asset. - [Save Scenes](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) — Persist your edited design as a scene file or an archive. - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Export the imported design to PNG, PDF, and other formats. --- ## Related Pages - [From InDesign](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-indesign-ba3988/) - Load Adobe InDesign (IDML) designs into CE.SDK by converting them to a scene archive on a server and importing the archive with the engine. - [From Photoshop](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-photoshop-cca6bb/) - Load Adobe Photoshop (PSD) designs into CE.SDK by converting them to a scene archive on a server and importing the archive with the engine. - [Import Design from Archive](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-archive-dde9fa/) - Load self-contained CE.SDK archive files that bundle scene structure with all referenced assets for portable, reliable design imports. --- ## 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 Design from Archive" description: "Load self-contained CE.SDK archive files that bundle scene structure with all referenced assets for portable, reliable design imports." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-archive-dde9fa/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [Import a Design](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design-73b9c5/) > [From Archive](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-archive-dde9fa/) --- ```swift file=@cesdk_swift_examples/engine-guides-import-design-from-archive/ImportDesignFromArchive.swift reference-only import Foundation import IMGLYEngine @MainActor func importDesignFromArchive(engine: Engine) async throws { // Demo scaffolding: load a template so there is a design to archive and import. // In your app you would start from a scene already open in 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 archiveBlob = try await engine.scene.saveToArchive() let archiveURL = FileManager.default.temporaryDirectory.appendingPathComponent("design.imgly") try archiveBlob.write(to: archiveURL) try await engine.scene.load(from: archiveURL) let dataURL = FileManager.default.temporaryDirectory.appendingPathComponent("design-from-data.imgly") try archiveBlob.write(to: dataURL) try await engine.scene.load(from: dataURL) let textBlocks = try engine.block.find(byType: .text) if let firstTextBlock = textBlocks.first { try engine.block.replaceText(firstTextBlock, text: "Loaded from Archive") } } ``` Import archived CE.SDK scenes that bundle the design structure together with all fonts, images, and assets in a single portable file. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-import-design-from-archive) Scene files reference assets by URL, so they break when those URLs become unavailable. Archives solve this by packaging the scene together with every font, image, and video into a single file — saved with the `.imgly` extension (the `.zip` extension also works). This makes a design self-contained and reliable to move between environments, share with others, or store long term. ## Understanding CE.SDK Archives A CE.SDK archive is a ZIP container created with `engine.scene.saveToArchive()` — save it with the `.imgly` extension — that holds both the scene structure and all referenced assets. The scene points at those assets with relative paths, so the archive carries no external URL dependencies and loads identically wherever it is hosted. An archive uses a predictable directory layout: - `scene.scene` — the scene structure, layout, and element properties - `images/` — image assets used in the scene - `fonts/` — font files used by text blocks - `videos/` — video content referenced in the scene - `audios/` — audio tracks and sound effects ## Create an Archive Produce an archive with `engine.scene.saveToArchive()`. It returns the current design and all its assets as a `Blob` (a typealias for `Data`), which you can write to disk to load back later. ```swift highlight-importDesignFromArchive-createArchive let archiveBlob = try await engine.scene.saveToArchive() let archiveURL = FileManager.default.temporaryDirectory.appendingPathComponent("design.imgly") try archiveBlob.write(to: archiveURL) ``` See the [Save a Scene](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) guide for the full save story, including scene files and compression options. ## Load an Archive Use `engine.scene.load(from:)` to import an archive — the same call that loads scene files, since the engine detects the file kind automatically. The engine fetches the archive, extracts its contents, and loads the scene with all bundled assets as the active scene. ```swift highlight-importDesignFromArchive-loadFromURL try await engine.scene.load(from: archiveURL) ``` The URL can point to a file on disk — a bundled file, or one the user selected — or to a remote `https://` location, which the engine downloads before loading. Loading is asynchronous and replaces the current scene. Wrap the call in a `do`/`catch` to handle a failed load, such as a corrupt archive or an unreachable URL. ## Load an Archive from In-Memory Data When you hold the archive as in-memory `Data` rather than a URL — bytes downloaded from your API or read from a database — write it to a temporary file and load that file. ```swift highlight-importDesignFromArchive-loadFromData let dataURL = FileManager.default.temporaryDirectory.appendingPathComponent("design-from-data.imgly") try archiveBlob.write(to: dataURL) try await engine.scene.load(from: dataURL) ``` ## Modify the Loaded Scene Once an archive loads, the scene is immediately editable. Locate elements with the block API and update them like any other scene. ```swift highlight-importDesignFromArchive-modify let textBlocks = try engine.block.find(byType: .text) if let firstTextBlock = textBlocks.first { try engine.block.replaceText(firstTextBlock, text: "Loaded from Archive") } ``` Image fills reference the bundled assets through relative URIs, so they remain available wherever the archive is stored. ## Archive Contents and Structure Archives use standard ZIP compression with a fixed internal layout. The scene file references assets by relative path instead of absolute URL: ```text archive.zip ├── scene.scene ├── images/ │ ├── image-abc123.jpg │ └── image-def456.png ├── fonts/ │ └── CustomFont-Regular.ttf ├── videos/ │ └── video-ghi789.mp4 └── audios/ └── audio-jkl012.mp3 ``` A relative URI such as `./images/image-abc123.jpg` resolves from within the archive, so bundled assets are always accessible no matter where the archive lives. ## Archives vs Scene Files CE.SDK offers two save formats that handle assets differently: - **Scene files** are lightweight JSON that store the design structure and reference assets by their original URLs. Use them when those URLs stay reachable and you want the smallest possible file. - **Archives** bundle the scene with all referenced assets using relative paths. Use them when you need a self-contained, portable package. Both kinds are saved with the `.imgly` extension; `.scene` and `.zip` files also load. Load either with the same `engine.scene.load(from:)` call — the engine detects the file kind automatically. See [Load a Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/) for the scene-file path. ## Asset Availability and Portability A scene file only loads correctly while every referenced asset stays at its original URL. Moving assets, expiring authentication, or losing network access all break it. Archives remove that dependency by bundling the assets inside the archive file, which makes a design environment-independent, offline-capable, and safe to share without coordinating asset access. ## Troubleshooting **The archive fails to load** — confirm it was created with `engine.scene.saveToArchive()`, since CE.SDK archives use a specific internal structure that an arbitrary `.zip` does not have. Check that the file is a valid, uncorrupted archive, and that a remote URL is reachable. **Assets are missing after loading** — verify the archive was not edited by hand, which breaks the relative references, and that the asset formats and codecs are supported. **Large archives take time** — loading is asynchronous because the engine downloads and extracts the archive. Show a progress indicator in your UI while the load completes. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.load(from:)` | Load a scene file or archive from a URL (file kind detected automatically) | | `engine.scene.saveToArchive()` | Create an archive as `Data` (the `Blob` typealias) bundling the scene with its assets | | `engine.scene.loadArchive(from:)` | Load a scene archive from a URL | | `engine.block.find(byType:)` | Find blocks by type in the loaded scene | ## Next Steps - [Save a Scene](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) — Create archives and scene files from the current design. - [Load a Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/) — Load a scene file when assets are referenced by URL. --- ## 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: "From InDesign" description: "Load Adobe InDesign (IDML) designs into CE.SDK by converting them to a scene archive on a server and importing the archive with the engine." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-indesign-ba3988/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [Import a Design](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design-73b9c5/) > [From InDesign](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-indesign-ba3988/) --- ```swift file=@cesdk_swift_examples/engine-guides-import-from-indesign/ImportFromInDesign.swift reference-only import Foundation import IMGLYEngine @MainActor func importFromInDesign(engine: Engine) async throws { // Stand in for the .imgly archive your server produces from an IDML file with // the @imgly/idml-importer package. In production, archiveURL points to that // archive — a remote URL on your CDN or a local file URL — and // load(from:) accepts either. 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) let archiveData = try await engine.scene.saveToArchive() let archiveURL = FileManager.default.temporaryDirectory .appendingPathComponent("converted-indesign-\(UUID().uuidString).imgly") try archiveData.write(to: archiveURL) try await engine.scene.load(from: archiveURL) let pages = try engine.scene.getPages() print("Imported design has \(pages.count) page(s)") guard let scene = try engine.scene.get() else { return } try await engine.scene.zoom( to: scene, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) } ``` Bring Adobe InDesign designs into CE.SDK by converting IDML files to a scene archive on a server, then loading that archive into the engine. > **Reading time:** 4 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-import-from-indesign) InDesign import is handled by the `@imgly/idml-importer` package, which parses IDML files and converts them into CE.SDK scenes. That package runs in Node.js and the browser — there is no on-device IDML parser. The recommended workflow is to convert IDML files to a portable `.imgly` archive once with the [Node.js importer](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-indesign-ba3988/), then ship or download that archive and load it with `engine.scene.load(from:)`. ## Load the Converted Archive Load the `.imgly` archive produced by the conversion step with `load(from:)`. Archives are ZIP files that bundle the scene together with its embedded assets, so the import is self-contained — the URL can point to a local file or a remote download. ```swift highlight-importFromInDesign-loadArchive try await engine.scene.load(from: archiveURL) ``` ## Verify the Import After loading, confirm the scene contains pages before presenting it. `engine.scene.getPages()` returns the imported pages; an empty result means the archive held no usable content. ```swift highlight-importFromInDesign-verifyImport let pages = try engine.scene.getPages() print("Imported design has \(pages.count) page(s)") ``` ## Fit the Scene to the Viewport Retrieve the current scene with `engine.scene.get()` and frame it with `engine.scene.zoom(to:)`. The four padding parameters add space in points around the focused block. ```swift highlight-importFromInDesign-fitViewport guard let scene = try engine.scene.get() else { return } try await engine.scene.zoom( to: scene, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) ``` ## What Gets Imported The conversion preserves element grouping, positioning, rotation, and transparency, along with text (bold and italic styles), shapes (rectangles, ovals, polygons, lines), solid and gradient fills, strokes, and embedded images. These are baked into the archive during conversion, so the imported scene is fully editable once loaded. ## Limitations The same conversion limitations apply wherever you load the result: - **Linked images** become placeholders. Embed all images in InDesign before exporting to IDML. - **Text flow** between multiple frames is not supported and may appear duplicated. - **Unavailable fonts** are substituted with fallbacks. Configure font matching during conversion for the best results. - **Complex text formatting** beyond bold and italic may not be preserved. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.load(from:)` | Load a scene and its bundled assets from an `.imgly` archive URL | | `engine.scene.get()` | Return the current scene block, or `nil` if none is loaded | | `engine.scene.getPages()` | Return the pages of the current scene | | `engine.scene.zoom(to:paddingLeft:paddingTop:paddingRight:paddingBottom:)` | Fit a block in the viewport with padding in points | ## Next Steps - [Import Design from Archive](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-archive-dde9fa/) — Learn the full `.imgly` archive workflow used by this guide. - [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. - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Export your imported design to PNG, 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: "From Photoshop" description: "Load Adobe Photoshop (PSD) designs into CE.SDK by converting them to a scene archive on a server and importing the archive with the engine." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-photoshop-cca6bb/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [Import a Design](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design-73b9c5/) > [From Photoshop](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-photoshop-cca6bb/) --- ```swift file=@cesdk_swift_examples/engine-guides-import-from-photoshop/ImportFromPhotoshop.swift reference-only import Foundation import IMGLYEngine @MainActor func importFromPhotoshop(engine: Engine) async throws { // Stand in for the .imgly archive your server produces from a PSD file with // the @imgly/psd-importer package. In production, archiveURL points to that // archive — a remote URL on your CDN or a local file URL — and // load(from:) accepts either. 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) let archiveData = try await engine.scene.saveToArchive() let archiveURL = FileManager.default.temporaryDirectory .appendingPathComponent("converted-photoshop-\(UUID().uuidString).imgly") try archiveData.write(to: archiveURL) try await engine.scene.load(from: archiveURL) let pages = try engine.scene.getPages() print("Imported design has \(pages.count) page(s)") guard let scene = try engine.scene.get() else { return } try await engine.scene.zoom( to: scene, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) } ``` Bring Adobe Photoshop designs into CE.SDK by converting PSD files to a scene archive on a server, then loading that archive into the engine. > **Reading time:** 4 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-import-from-photoshop) Photoshop import is handled by the `@imgly/psd-importer` package, which parses PSD files and converts them into CE.SDK scenes. That package runs in Node.js and the browser — there is no on-device PSD parser. The recommended workflow is to convert PSD files to a portable `.imgly` archive once with the [Node.js importer](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-photoshop-cca6bb/), then ship or download that archive and load it with `engine.scene.load(from:)`. ## Load the Converted Archive Load the `.imgly` archive produced by the conversion step with `load(from:)`. Point `archiveURL` at that file — a bundled archive resolved with `Bundle.main.url(forResource:withExtension:)`, or a remote download from your server. Archives are ZIP files that bundle the scene together with its embedded assets, so the import is self-contained — `load(from:)` accepts a local file URL or a remote one. ```swift highlight-importFromPhotoshop-loadArchive try await engine.scene.load(from: archiveURL) ``` ## Verify the Import After loading, confirm the scene contains pages before presenting it. `engine.scene.getPages()` returns the imported pages; an empty result means the loaded scene contains no pages. ```swift highlight-importFromPhotoshop-verifyImport let pages = try engine.scene.getPages() print("Imported design has \(pages.count) page(s)") ``` ## Fit the Scene to the Viewport Retrieve the current scene with `engine.scene.get()` and frame it with `engine.scene.zoom(to:)`. The four padding parameters add space in points around the focused block. ```swift highlight-importFromPhotoshop-fitViewport guard let scene = try engine.scene.get() else { return } try await engine.scene.zoom( to: scene, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) ``` ## What Gets Imported The conversion preserves layer grouping, positioning, rotation, and transparency, along with text (font family with bold and italic styles), shapes (rectangles, ovals, polygons, lines, and custom shapes), solid color fills, strokes, and embedded images. These are baked into the archive during conversion, so the imported scene is fully editable once loaded. ## Limitations The same conversion limitations apply wherever you load the result: - **Gradient fills** are not supported — only solid color fills are converted. - **Image cropping** is not preserved; images import at their full bounds. - **Text** within a single layer cannot mix multiple font sizes or families, and text justification is not supported. - **Groups** have limited support, especially single-member groups. - **Unavailable fonts** are substituted with fallbacks. Configure Google Fonts matching during conversion for the best results. - **Some blend modes** are not supported, including PassThrough, Dissolve, Linear Burn, and Subtract. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.load(from:)` | Load a scene and its bundled assets from an `.imgly` archive URL | | `engine.scene.get()` | Return the current scene block, or `nil` if none is loaded | | `engine.scene.getPages()` | Return the pages of the current scene | | `engine.scene.zoom(to:paddingLeft:paddingTop:paddingRight:paddingBottom:)` | Fit a block in the viewport with padding in points | ## Next Steps - [Import Design from Archive](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-archive-dde9fa/) — Learn the full `.imgly` archive workflow used by this guide. - [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. - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Export your imported design to PNG, 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: "Load a Scene" description: "Load existing design scenes into the editor to resume or modify previous work." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [Load a Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/) --- ```swift file=@cesdk_swift_examples/engine-guides-load-scene-from-remote/LoadSceneFromRemote.swift reference-only import Foundation import IMGLYEngine @MainActor func loadSceneFromRemote(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 text = try engine.block.find(byType: .text).first else { return } try engine.block.setDropShadowEnabled(text, enabled: true) } ``` ```swift file=@cesdk_swift_examples/engine-guides-load-scene-from-string/LoadSceneFromString.swift reference-only import Foundation import IMGLYEngine @MainActor func loadSceneFromString(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") let sceneBlob = try await URLSession.shared.data(from: sceneURL).0 guard let blobString = String(data: sceneBlob, encoding: .utf8) else { return } try await engine.scene.load(from: blobString) let text = try engine.block.find(byType: .text).first! try engine.block.setDropShadowEnabled(text, enabled: true) } ``` ```swift file=@cesdk_swift_examples/engine-guides-load-scene-from-blob/LoadSceneFromBlob.swift reference-only import Foundation import IMGLYEngine @MainActor func loadSceneFromBlob(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") let sceneBlob = try await URLSession.shared.data(from: sceneURL).0 guard let blobString = String(data: sceneBlob, encoding: .utf8) else { return } try await engine.scene.load(from: blobString) let text = try engine.block.find(byType: .text).first! try engine.block.setDropShadowEnabled(text, enabled: true) } ``` Load previously saved scenes to resume editing or adapt existing designs. The CE.SDK Engine loads scenes from a remote URL, a string, or a data blob, and a loaded scene is immediately editable. > **Reading time:** 4 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-load-scene-from-remote) Scene files contain layout, properties, and asset references, but not the assets themselves. When loading a scene, make sure the referenced asset URLs remain accessible. For self-contained packages with bundled assets, use archives instead. This guide covers loading scenes from URLs, strings, and blobs, and modifying a loaded scene. ## Load a Scene from URL The most common approach loads a scene from a remote URL. Pass a URL that points to a scene file — an `.imgly` or `.scene` file — to `engine.scene.load(from:)`. The call is asynchronous and replaces any existing scene with the loaded one. It throws if the scene cannot be loaded. ```swift highlight-load-remote try await engine.scene.load(from: sceneURL) ``` ## Load a Scene from String When a scene is stored in a database or local storage, load it from a string — typically the output of a previous `engine.scene.saveToString()` call. Here, fetch the scene over the network and decode it to a string. ```swift highlight-fetch-string let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") let sceneBlob = try await URLSession.shared.data(from: sceneURL).0 guard let blobString = String(data: sceneBlob, encoding: .utf8) else { return } ``` Pass the string to `engine.scene.load(from:)`. As with the URL form, the editor resets and presents the loaded scene. ```swift highlight-load-string try await engine.scene.load(from: blobString) ``` ## Load a Scene from In-Memory Data When you already hold the scene's bytes in memory — from a file upload or blob storage — start from `Data`. Here, fetch the bytes to stand in for that in-memory data. ```swift highlight-fetch-blob let sceneURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") let sceneBlob = try await URLSession.shared.data(from: sceneURL).0 ``` `load(from:)` accepts a `URL` or a `String`, but not raw `Data`, so decode the bytes to a UTF-8 string first. ```swift highlight-read-blob guard let blobString = String(data: sceneBlob, encoding: .utf8) else { return } ``` Then load it with `engine.scene.load(from:)`. ```swift highlight-load-blob try await engine.scene.load(from: blobString) ``` ## Modify a Loaded Scene After loading, the scene is immediately editable. Locate elements with `engine.block.find(byType:)`, then change them with the block APIs. This example adds a drop shadow to the first text block. ```swift highlight-modify-text-remote guard let text = try engine.block.find(byType: .text).first else { return } try engine.block.setDropShadowEnabled(text, enabled: true) ``` A scene load can be reverted with `engine.editor.undo()`. ## Scene Files vs Archives Scene files are lightweight: they store only references to assets, so the scene won't display correctly if those asset URLs become unavailable. For a self-contained package with bundled assets, load an archive with the same `engine.scene.load(from:)` call — the engine detects the file kind automatically, and all asset paths resolve relative to the archive's location. Both scenes and archives use the `.imgly` extension; `.scene` and `.zip` files also load. See [Import Design from Archive](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design/from-archive-dde9fa/) for the full archive workflow. To redirect asset requests to a different location, register a custom resolver; see the [URI Resolver](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/uri-resolver-36b624/) guide. ## Troubleshooting ### Scene fails to load - Verify the URL is reachable and returns a valid `.scene` file. - Ensure the scene format is compatible with your CE.SDK version. ### Assets not displaying after load - Scene files store asset references as URLs — make sure those URLs remain accessible. - Use archives for self-contained scenes with bundled assets. - Configure a [URI resolver](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/uri-resolver-36b624/) if assets are hosted on a different server. ### String content is invalid - Ensure the string is the exact output of `engine.scene.saveToString()`. - Verify the string was not modified or truncated during storage. ## API Reference | Method | Description | | --- | --- | | `engine.scene.load(from: URL)` | Load a scene or archive from a URL (file kind detected automatically) | | `engine.scene.load(from: String)` | Load a scene from a string | | `engine.scene.loadArchive(from: URL)` | Load a scene archive from a URL | | `engine.block.find(byType:)` | Find blocks by type | | `engine.block.setDropShadowEnabled(_:enabled:)` | Enable or disable a block's drop shadow | | `engine.editor.undo()` | Revert a scene load | ## Next Steps - [Save Scenes](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) — Persist your work as a scene file or archive for later loading. - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Edit blocks, properties, and content in a loaded 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: "Overview" description: "Learn how to load and create scenes, set the zoom level, and configure URI resolvers." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/overview-99444b/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/overview-99444b/) --- CreativeEditor SDK (CE.SDK) offers multiple ways to open the editor. Whether you're starting with a blank canvas or importing complex layered files, CE.SDK gives you the building blocks to launch an editing session tailored to your users' needs. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## Ways to Open the Editor You can initialize CE.SDK in several ways depending on your content pipeline: - [Start with a Blank Canvas](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/blank-canvas-18ff05/)
Create new content from scratch by defining the canvas dimensions manually or programmatically. - [Load a Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/)
Restore a saved scene from serialized scene data, a scene file, or a self-contained archive. - Create from Media
Initialize the editor with a preloaded [image](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-image-ad9b5e/) or [video](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-video-86beb0/). - [Create from Template](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/from-template-46c096/)
Kick off the editor with a predefined template, including placeholders and editing constraints. - [Import a Design](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/import-design-73b9c5/)
Import external designs with the relevant importer, then load the resulting scene or archive in the SDK. ## Set the Zoom Level After a scene is open, use [Set Zoom Level](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/set-zoom-level-d31896/) to control the viewport and focus the canvas on the content your user should inspect next. ## Using Low-Quality / High-Quality Assets To ensure responsive editing and high-quality exports, CE.SDK allows you to dynamically switch between asset resolutions: - **Edit with Low-Res Assets**
Load smaller versions of images or videos during the editing process to reduce memory usage and improve performance. - **Export with High-Res Assets**
Swap out low-res placeholders for full-quality assets just before exporting. This can be handled using the Scene or Block APIs by switching asset paths or making use of source sets for fills. > **Note:** This pattern is commonly used in design systems that require high-resolution > print or web output while maintaining editing performance. ## Working with Watermarked or Placeholder Media CE.SDK supports licensing-based workflows where full-resolution assets are only available after purchase or user action: - **Use Watermarked or Preview Media on Load**
Start with branded, obfuscated, or watermarked assets to limit unauthorized use. - **Swap with Purchased Assets Post-Checkout**
Replace asset URIs within the same scene structure using a one-time update, ensuring consistency without disrupting layout or styling. ## Implementing a Custom URI Resolver Use [URI resolver APIs](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/uri-resolver-36b624/) to intercept and customize asset loading: - **Why Use a URI Resolver?**
Handle dynamic URL rewriting, signed query parameters, asset migration, CDN fallbacks, or redirects to internal mirrors. - **How It Works**
The engine routes every asset URI through your custom resolver function. This function returns the final, resolved URI used for the current fetch operation. - **Recommended Use Cases**: - Append signed query params - Redirect public assets to internal mirrors - Refresh tokenized URLs before they expire --- ## 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 Zoom Level" description: "Control the canvas zoom level with the CE.SDK Engine — set and read zoom, frame blocks, follow content with auto-fit, clamp the camera, and observe zoom changes." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/set-zoom-level-d31896/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [Set Zoom Level](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/set-zoom-level-d31896/) --- ```swift file=@cesdk_swift_examples/engine-guides-set-zoom-level/SetZoomLevel.swift reference-only import Foundation import IMGLYEngine @MainActor func setZoomLevel(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 graphic = try engine.block.create(.graphic) try engine.block.setShape(graphic, shape: try engine.block.createShape(.rect)) try engine.block.setFill(graphic, fill: try engine.block.createFill(.color)) try engine.block.setWidth(graphic, value: 300) try engine.block.setHeight(graphic, value: 300) try engine.block.appendChild(to: page, child: graphic) try engine.scene.setZoom(1.0) let currentZoom = try engine.scene.getZoom() try engine.scene.setZoom(0.5 * currentZoom) try await engine.scene.zoom( to: page, paddingLeft: 20, paddingTop: 20, paddingRight: 20, paddingBottom: 20, ) try engine.scene.immediateZoom( to: page, paddingLeft: 20, paddingTop: 20, paddingRight: 20, paddingBottom: 20, forceUpdate: true, ) try engine.scene.enableZoomAutoFit( page, axis: .both, paddingLeft: 20, paddingTop: 20, paddingRight: 20, paddingBottom: 20, ) let autoFitEnabled = try engine.scene.isZoomAutoFitEnabled(page) print("Auto-fit enabled: \(autoFitEnabled)") try engine.scene.disableZoomAutoFit(page) try engine.scene.unstable_enableCameraZoomClamping( [page], minZoomLimit: 0.125, maxZoomLimit: 8.0, ) let zoomClampingEnabled = try engine.scene.unstable_isCameraZoomClampingEnabled(scene) print("Zoom clamping enabled: \(zoomClampingEnabled)") try engine.scene.unstable_disableCameraZoomClamping() try engine.scene.unstable_enableCameraPositionClamping( [scene], paddingLeft: 10, paddingTop: 10, paddingRight: 10, paddingBottom: 10, ) let positionClampingEnabled = try engine.scene.unstable_isCameraPositionClampingEnabled(scene) print("Position clamping enabled: \(positionClampingEnabled)") try engine.scene.unstable_disableCameraPositionClamping() let zoomTask = Task { for await _ in engine.scene.onZoomLevelChanged { let zoom = try engine.scene.getZoom() print("Zoom level changed: \(zoom)") } } try engine.scene.setZoom(2.0) zoomTask.cancel() } ``` Control how much of a design is visible by driving the camera zoom from code. Set an exact zoom level, frame a block, follow content as it resizes, constrain the camera, and react to zoom changes — all through the Engine `scene` API. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-set-zoom-level) The zoom level is a ratio between design pixels and screen pixels. A zoom level of `1.0` shows one design pixel as one screen pixel; `2.0` shows it as two. Every call below operates on the engine's active scene and reads back through `engine.scene`. ## Get and Set the Zoom Level Set an absolute zoom level with `setZoom(_:)` and read the current one with `getZoom()`. Reading the current value first lets you apply a relative change, such as halving the zoom. ```swift highlight-setZoomLevel-getSet try engine.scene.setZoom(1.0) let currentZoom = try engine.scene.getZoom() try engine.scene.setZoom(0.5 * currentZoom) ``` ## Zoom to a Block Frame a specific block — the page, a group, or any element — with `zoom(to:paddingLeft:paddingTop:paddingRight:paddingBottom:)`. Without padding the camera fits the block tightly; padding (in points) leaves breathing room on each side. `immediateZoom(to:…forceUpdate:)` performs the same framing synchronously. It assumes the layout is already up to date; pass `forceUpdate: true` to run a layout pass first. ```swift highlight-setZoomLevel-zoomToBlock try await engine.scene.zoom( to: page, paddingLeft: 20, paddingTop: 20, paddingRight: 20, paddingBottom: 20, ) try engine.scene.immediateZoom( to: page, paddingLeft: 20, paddingTop: 20, paddingRight: 20, paddingBottom: 20, forceUpdate: true, ) ``` ## Auto-Fit Zoom Auto-fit continuously refits a block as its bounding box changes. Choose the axis to follow with `ZoomAutoFitAxis` — `.both`, `.horizontal`, or `.vertical`. Auto-fit only takes effect while the zoom level is not being driven by an editor UI layer, and calling `setZoom(_:)` or `zoom(to:)` disables it. ```swift highlight-setZoomLevel-autoFit try engine.scene.enableZoomAutoFit( page, axis: .both, paddingLeft: 20, paddingTop: 20, paddingRight: 20, paddingBottom: 20, ) ``` ## Disable Auto-Fit Stop following a block with `disableZoomAutoFit(_:)`, and check whether auto-fit is currently enabled with `isZoomAutoFitEnabled(_:)`. ```swift highlight-setZoomLevel-disableAutoFit let autoFitEnabled = try engine.scene.isZoomAutoFitEnabled(page) print("Auto-fit enabled: \(autoFitEnabled)") try engine.scene.disableZoomAutoFit(page) ``` ## Limit the Zoom Range `unstable_enableCameraZoomClamping(_:minZoomLimit:maxZoomLimit:…)` keeps the camera zoom within a range relative to the given blocks. A negative limit means unbounded. Query whether clamping is active with `unstable_isCameraZoomClampingEnabled(_:)` and remove it with `unstable_disableCameraZoomClamping()`. These camera-clamping APIs are experimental, indicated by the `unstable_` prefix. ```swift highlight-setZoomLevel-zoomClamping try engine.scene.unstable_enableCameraZoomClamping( [page], minZoomLimit: 0.125, maxZoomLimit: 8.0, ) let zoomClampingEnabled = try engine.scene.unstable_isCameraZoomClampingEnabled(scene) print("Zoom clamping enabled: \(zoomClampingEnabled)") try engine.scene.unstable_disableCameraZoomClamping() ``` ## Constrain the Camera Position `unstable_enableCameraPositionClamping(_:…)` keeps the camera within the bounds of the given blocks, so panning never leaves the content. Padding (in points) defines how far past the bounds the camera may move. Query with `unstable_isCameraPositionClampingEnabled(_:)` and remove with `unstable_disableCameraPositionClamping()`. Camera clamping also integrates with the editor's global settings — see the [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) guide. ```swift highlight-setZoomLevel-positionClamping try engine.scene.unstable_enableCameraPositionClamping( [scene], paddingLeft: 10, paddingTop: 10, paddingRight: 10, paddingBottom: 10, ) let positionClampingEnabled = try engine.scene.unstable_isCameraPositionClampingEnabled(scene) print("Position clamping enabled: \(positionClampingEnabled)") try engine.scene.unstable_disableCameraPositionClamping() ``` ## Subscribe to Zoom Changes `onZoomLevelChanged` is an `AsyncStream` that emits whenever the zoom level changes. Iterate it in a `Task` to keep custom UI — a zoom indicator, say — in sync, and cancel the task when you no longer need updates. ```swift highlight-setZoomLevel-subscribe let zoomTask = Task { for await _ in engine.scene.onZoomLevelChanged { let zoom = try engine.scene.getZoom() print("Zoom level changed: \(zoom)") } } try engine.scene.setZoom(2.0) zoomTask.cancel() ``` ## API Reference ### Methods | Method | Description | | --- | --- | | `setZoom(_:)` | Set the active scene's zoom level, in unit `1/px`. | | `getZoom()` | Read the active scene's current zoom level. | | `zoom(to:paddingLeft:paddingTop:paddingRight:paddingBottom:)` | Animate the camera to frame a block, with optional per-side padding in points. | | `immediateZoom(to:paddingLeft:paddingTop:paddingRight:paddingBottom:forceUpdate:)` | Frame a block without animation; pass `forceUpdate: true` to run a layout pass first. | | `enableZoomAutoFit(_:axis:paddingLeft:paddingTop:paddingRight:paddingBottom:)` | Continuously refit a block on the given `ZoomAutoFitAxis`. | | `disableZoomAutoFit(_:)` | Stop a previously enabled auto-fit. | | `isZoomAutoFitEnabled(_:)` | Query whether auto-fit is enabled. | | `unstable_enableCameraZoomClamping(_:minZoomLimit:maxZoomLimit:…)` | Constrain the zoom range relative to the given blocks. Experimental. | | `unstable_disableCameraZoomClamping()` | Remove zoom clamping. Experimental. | | `unstable_isCameraZoomClampingEnabled(_:)` | Query whether zoom clamping is enabled. Experimental. | | `unstable_enableCameraPositionClamping(_:…)` | Keep the camera within the bounds of the given blocks. Experimental. | | `unstable_disableCameraPositionClamping()` | Remove position clamping. Experimental. | | `unstable_isCameraPositionClampingEnabled(_:)` | Query whether position clamping is enabled. Experimental. | ### Properties | Property | Type | Description | | --- | --- | --- | | `onZoomLevelChanged` | `AsyncStream` | Emits whenever the zoom level changes. | ## Troubleshooting | Problem | Resolution | | --- | --- | | Zoom level doesn't change | Confirm a scene exists before calling zoom methods, and that no editor UI layer is overriding the zoom. | | Auto-fit has no effect | Only one block per scene can drive auto-fit, and `setZoom(_:)` or `zoom(to:)` disables it. Pass a valid block that belongs to the active scene. | | Zoom feels capped | An active zoom clamp limits the range. Check with `unstable_isCameraZoomClampingEnabled(_:)` and adjust or remove it. | ## Next Steps - [Start With Blank Canvas](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/blank-canvas-18ff05/) — Start the editor with an empty scene to zoom into. - [Load a Scene](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/load-scene-478833/) — Open an existing design before adjusting the camera. --- ## 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: "URI Resolver" description: "Intercept and transform asset URIs before CE.SDK loads them — for authentication, redirects, and custom resolution logic." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/uri-resolver-36b624/" --- > 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/) > [Open the Editor](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor-23a1db/) > [URI Resolver](https://img.ly/docs/cesdk/mac-catalyst/open-the-editor/uri-resolver-36b624/) --- ```swift file=@cesdk_swift_examples/engine-guides-uri-resolver/URIResolver.swift reference-only import Foundation import IMGLYEngine @MainActor func uriResolver(engine: Engine) async throws { // Resolve a path without loading the asset. With no custom resolver, a relative // path is prefixed with the `basePath` setting and absolute paths pass through. let resolved = try await engine.editor.getAbsoluteURI(relativePath: "/banana.jpg") print(resolved) try engine.editor.setURIResolver { [weak engine] uri in // Rewrite every .jpg request to the IMG.LY logo. if uri.hasSuffix(".jpg") { return URL(string: "https://img.ly/static/ubq_samples/imgly_logo.jpg")! } // Delegate everything else to the default resolution behavior. guard let engine else { return URL(string: uri)! } return URL(string: engine.editor.defaultURIResolver(relativePath: uri))! } // The resolver runs for every request, so .jpg paths now resolve to the logo // whether they are relative or absolute. print(try await engine.editor.getAbsoluteURI(relativePath: "/banana.jpg")) // Pre-compute the token; a synchronous resolver can't await a network call. let accessToken = "" try engine.editor.setURIResolver { [weak engine] uri in guard let engine else { return URL(string: uri)! } let absoluteURI = engine.editor.defaultURIResolver(relativePath: uri) // Only protected assets need the token; pass everything else through unchanged. guard uri.contains("/protected/"), var components = URLComponents(string: absoluteURI) else { return URL(string: absoluteURI)! } components.queryItems = (components.queryItems ?? []) + [URLQueryItem(name: "token", value: accessToken)] return components.url ?? URL(string: absoluteURI)! } // When the token or signed URL must be fetched per request, use the async // resolver and await your backend. Replace the stand-in with a real request. let requestSignedURL: @Sendable (String) async throws -> URL = { absoluteURI in URL(string: absoluteURI)! } try engine.editor.setURIResolverAsync { [weak engine] uri in guard let engine else { throw URLError(.cancelled) } let absoluteURI = await engine.editor.defaultURIResolver(relativePath: uri) guard uri.contains("/protected/") else { return URL(string: absoluteURI)! } return try await requestSignedURL(absoluteURI) } // Pass nil to remove the custom resolver and restore the default behavior. try engine.editor.setURIResolver(nil) print(try await engine.editor.getAbsoluteURI(relativePath: "/banana.jpg")) } ``` Learn how to intercept and transform asset URIs in CE.SDK, enabling authentication and custom resolution logic. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-uri-resolver) When CE.SDK loads an asset, it resolves the requested URI to an absolute URL before fetching it. You can intercept this step to add authentication tokens, redirect to a different host, or transform URIs to match your application's needs. ## Default URI Resolution By default, CE.SDK resolves URIs relative to the `basePath` setting: absolute URIs (with a scheme such as `https://` or `file://`) pass through unchanged, while relative paths are prefixed with `basePath`. Use `getAbsoluteURI(relativePath:)` to preview how a path resolves without loading the asset: ```swift highlight-test-resolution // Resolve a path without loading the asset. With no custom resolver, a relative // path is prefixed with the `basePath` setting and absolute paths pass through. let resolved = try await engine.editor.getAbsoluteURI(relativePath: "/banana.jpg") print(resolved) ``` ## Custom URI Resolver Register a custom resolver with `setURIResolver(_:)`. CE.SDK then routes every requested URI through your closure and fetches whatever URL you return. The resolved URL is used only for that request and is never stored. Return `defaultURIResolver(relativePath:)` for any path you don't transform, otherwise it would be lost. ```swift highlight-set-resolver try engine.editor.setURIResolver { [weak engine] uri in // Rewrite every .jpg request to the IMG.LY logo. if uri.hasSuffix(".jpg") { return URL(string: "https://img.ly/static/ubq_samples/imgly_logo.jpg")! } // Delegate everything else to the default resolution behavior. guard let engine else { return URL(string: uri)! } return URL(string: engine.editor.defaultURIResolver(relativePath: uri))! } // The resolver runs for every request, so .jpg paths now resolve to the logo // whether they are relative or absolute. print(try await engine.editor.getAbsoluteURI(relativePath: "/banana.jpg")) ``` The resolver must return a `URL` that includes a scheme. The engine retains the resolver for as long as it is set, so capture anything that owns the engine weakly — for example `[weak self]` — to avoid a retain cycle. ## Adding Authentication A common use case is attaching an authentication token to protected asset URIs. Because `setURIResolver(_:)` runs a closure that can't `await`, generate the token ahead of time and append it as a query parameter. Filter to your own protected paths so the token isn't sent to public or third-party hosts: ```swift highlight-auth-resolver // Pre-compute the token; a synchronous resolver can't await a network call. let accessToken = "" try engine.editor.setURIResolver { [weak engine] uri in guard let engine else { return URL(string: uri)! } let absoluteURI = engine.editor.defaultURIResolver(relativePath: uri) // Only protected assets need the token; pass everything else through unchanged. guard uri.contains("/protected/"), var components = URLComponents(string: absoluteURI) else { return URL(string: absoluteURI)! } components.queryItems = (components.queryItems ?? []) + [URLQueryItem(name: "token", value: accessToken)] return components.url ?? URL(string: absoluteURI)! } ``` Your asset server then validates the token and serves the protected asset. When the credential must be fetched per request — for example, exchanging a path for a short-lived pre-signed URL — use `setURIResolverAsync(_:)` and `await` your backend inside the closure: ```swift highlight-auth-resolver-async // When the token or signed URL must be fetched per request, use the async // resolver and await your backend. Replace the stand-in with a real request. let requestSignedURL: @Sendable (String) async throws -> URL = { absoluteURI in URL(string: absoluteURI)! } try engine.editor.setURIResolverAsync { [weak engine] uri in guard let engine else { throw URLError(.cancelled) } let absoluteURI = await engine.editor.defaultURIResolver(relativePath: uri) guard uri.contains("/protected/") else { return URL(string: absoluteURI)! } return try await requestSignedURL(absoluteURI) } ``` ## Removing a Resolver Pass `nil` to remove the custom resolver and restore the default behavior. `setURIResolver(_:)` and `setURIResolverAsync(_:)` share a single resolver slot, so `nil` clears whichever one is active: ```swift highlight-remove-resolver // Pass nil to remove the custom resolver and restore the default behavior. try engine.editor.setURIResolver(nil) print(try await engine.editor.getAbsoluteURI(relativePath: "/banana.jpg")) ``` ## Key Constraints - **Sync vs async**: `setURIResolver(_:)` takes a non-throwing `(String) -> URL` closure and can't report errors. Use `setURIResolverAsync(_:)` when you need to `await` a backend call or surface a failure. - **Return absolute URLs**: The resolver must return a `URL` with a scheme (`https://`, `file://`, …). The incoming path may be relative or even invalid. - **One resolver at a time**: Both setters write the same slot, so a new call replaces the previous resolver, and passing `nil` removes it. - **Delegate unmatched URIs**: Return `defaultURIResolver(relativePath:)` for any path you don't transform. ## Next Steps - [Serve Assets](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) — Host engine and content assets on your own servers instead of the IMG.LY CDN. - [Import From Remote Source](https://img.ly/docs/cesdk/mac-catalyst/import-media/from-remote-source-b65faf/) — Connect CE.SDK to external sources like servers or third-party platforms to import assets remotely. --- ## 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: "Outlines" description: "Enhance design elements with strokes, shadows, and glow effects to improve contrast and visual appeal." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/outlines-b7820c/" --- > 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/) > [Outlines](https://img.ly/docs/cesdk/mac-catalyst/outlines-b7820c/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/outlines/overview-dfeb12/) - Enhance design elements with strokes, shadows, and glow effects to improve contrast and visual appeal. - [Using Strokes](https://img.ly/docs/cesdk/mac-catalyst/outlines/strokes-c2e621/) - Add and customize outlines around shapes, text, or images using stroke settings. - [Shadows and Glows](https://img.ly/docs/cesdk/mac-catalyst/outlines/shadows-and-glows-6610fa/) - Apply drop shadows and glow effects to design blocks with the CE.SDK Engine API for depth, contrast, and emphasis. --- ## 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: "Enhance design elements with strokes, shadows, and glow effects to improve contrast and visual appeal." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/outlines/overview-dfeb12/" --- > 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/) > [Outlines](https://img.ly/docs/cesdk/mac-catalyst/outlines-b7820c/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/outlines/overview-dfeb12/) --- In CreativeEditor SDK (CE.SDK), *outlines* refer to visual enhancements added around design elements. They include strokes, shadows, and glows, each serving to emphasize, separate, or stylize content. Outlines help improve visibility, create visual contrast, and enhance the overall aesthetic of a design. You can add, edit, and remove outlines both through the CE.SDK user interface and programmatically via the API. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## Next Steps This page introduces outlines at a conceptual level. To apply each effect in code, follow the dedicated guides: - [Using Strokes](https://img.ly/docs/cesdk/mac-catalyst/outlines/strokes-c2e621/) — Add and customize solid outlines around shapes, text, and images with the stroke API. - [Shadows and Glows](https://img.ly/docs/cesdk/mac-catalyst/outlines/shadows-and-glows-6610fa/) — Apply drop shadows and glow effects to add depth and emphasis. --- ## 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: "Shadows and Glows" description: "Apply drop shadows and glow effects to design blocks with the CE.SDK Engine API for depth, contrast, and emphasis." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/outlines/shadows-and-glows-6610fa/" --- > 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/) > [Outlines](https://img.ly/docs/cesdk/mac-catalyst/outlines-b7820c/) > [Shadows and Glows](https://img.ly/docs/cesdk/mac-catalyst/outlines/shadows-and-glows-6610fa/) --- ```swift file=@cesdk_swift_examples/engine-guides-shadows-and-glows/ShadowsAndGlows.swift reference-only import Foundation import IMGLYEngine @MainActor func shadowsAndGlows(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) // A gradient page fill gives the shadows and glows a backdrop to stand out against. let gradientFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops(gradientFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.0, g: 0.75, b: 0.85, a: 1.0), stop: 0.0), GradientColorStop(color: .rgba(r: 0.95, g: 0.85, b: 0.7, a: 1.0), stop: 0.5), GradientColorStop(color: .rgba(r: 0.85, g: 0.55, b: 0.45, a: 1.0), stop: 1.0), ]) try engine.block.setFloat(gradientFill, property: "fill/gradient/linear/startPointX", value: 0) try engine.block.setFloat(gradientFill, property: "fill/gradient/linear/startPointY", value: 0) try engine.block.setFloat(gradientFill, property: "fill/gradient/linear/endPointX", value: 1) try engine.block.setFloat(gradientFill, property: "fill/gradient/linear/endPointY", value: 1) try engine.block.setFill(page, fill: gradientFill) let baseURL = try engine.guidesBaseURL // A title text block to carry the drop shadow. let textBlock = try engine.block.create(.text) try engine.block.replaceText(textBlock, text: "Shadows & Glows") try engine.block.setTextFontSize(textBlock, fontSize: 80) try engine.block.setTextColor(textBlock, color: .rgba(r: 1.0, g: 1.0, b: 1.0, a: 1.0)) try engine.block.setWidthMode(textBlock, mode: .auto) try engine.block.setHeightMode(textBlock, mode: .auto) try engine.block.setPositionX(textBlock, value: 40) try engine.block.setPositionY(textBlock, value: 40) try engine.block.appendChild(to: page, child: textBlock) let supportsDropShadow = try engine.block.supportsDropShadow(textBlock) print("Block supports drop shadow: \(supportsDropShadow)") if supportsDropShadow { try engine.block.setDropShadowEnabled(textBlock, enabled: true) let shadowEnabled = try engine.block.isDropShadowEnabled(textBlock) print("Drop shadow enabled: \(shadowEnabled)") try engine.block.setDropShadowColor(textBlock, color: .rgba(r: 0.0, g: 0.3, b: 0.4, a: 0.8)) let shadowColor: Color = try engine.block.getDropShadowColor(textBlock) print("Drop shadow color: \(shadowColor)") try engine.block.setDropShadowOffsetX(textBlock, offsetX: 6) try engine.block.setDropShadowOffsetY(textBlock, offsetY: 6) let offsetX = try engine.block.getDropShadowOffsetX(textBlock) let offsetY = try engine.block.getDropShadowOffsetY(textBlock) print("Drop shadow offset: \(offsetX), \(offsetY)") try engine.block.setDropShadowBlurRadiusX(textBlock, blurRadiusX: 12) try engine.block.setDropShadowBlurRadiusY(textBlock, blurRadiusY: 12) let blurX = try engine.block.getDropShadowBlurRadiusX(textBlock) let blurY = try engine.block.getDropShadowBlurRadiusY(textBlock) print("Drop shadow blur: \(blurX), \(blurY)") } try await engine.captureGuide(page, label: "after-drop-shadow") // An image block to carry the glow effect. let imageBlock = try engine.block.create(.graphic) try engine.block.setShape(imageBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(imageBlock, value: 440) try engine.block.setPositionY(imageBlock, value: 220) try engine.block.setWidth(imageBlock, value: 300) try engine.block.setHeight(imageBlock, value: 300) 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: baseURL.appendingPathComponent("ly.img.image/images/sample_4.jpg"), ) try engine.block.setFill(imageBlock, fill: imageFill) let supportsEffects = try engine.block.supportsEffects(imageBlock) print("Block supports effects: \(supportsEffects)") if supportsEffects { let glow = try engine.block.createEffect(.glow) try engine.block.appendEffect(imageBlock, effectID: glow) try engine.block.setFloat(glow, property: "effect/glow/size", value: 10) try engine.block.setFloat(glow, property: "effect/glow/amount", value: 0.7) try engine.block.setFloat(glow, property: "effect/glow/darkness", value: 0.25) } try await engine.captureGuide(page, label: "after-glow") // A second image block to carry both a drop shadow and a glow at once. let combinedBlock = try engine.block.create(.graphic) try engine.block.setShape(combinedBlock, shape: engine.block.createShape(.rect)) try engine.block.setPositionX(combinedBlock, value: 60) try engine.block.setPositionY(combinedBlock, value: 220) try engine.block.setWidth(combinedBlock, value: 300) try engine.block.setHeight(combinedBlock, value: 300) try engine.block.appendChild(to: page, child: combinedBlock) let combinedFill = try engine.block.createFill(.image) try engine.block.setURL( combinedFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_5.jpg"), ) try engine.block.setFill(combinedBlock, fill: combinedFill) if try engine.block.supportsDropShadow(combinedBlock) { try engine.block.setDropShadowEnabled(combinedBlock, enabled: true) try engine.block.setDropShadowColor(combinedBlock, color: .rgba(r: 0.0, g: 0.2, b: 0.3, a: 0.6)) try engine.block.setDropShadowOffsetX(combinedBlock, offsetX: 8) try engine.block.setDropShadowOffsetY(combinedBlock, offsetY: 8) try engine.block.setDropShadowBlurRadiusX(combinedBlock, blurRadiusX: 20) try engine.block.setDropShadowBlurRadiusY(combinedBlock, blurRadiusY: 20) try engine.block.setDropShadowClip(combinedBlock, clip: false) let clipsToShape = try engine.block.getDropShadowClip(combinedBlock) print("Drop shadow clips to shape: \(clipsToShape)") } if try engine.block.supportsEffects(combinedBlock) { let combinedGlow = try engine.block.createEffect(.glow) try engine.block.appendEffect(combinedBlock, effectID: combinedGlow) try engine.block.setFloat(combinedGlow, property: "effect/glow/size", value: 8) try engine.block.setFloat(combinedGlow, property: "effect/glow/amount", value: 0.5) try engine.block.setFloat(combinedGlow, property: "effect/glow/darkness", value: 0.15) } try await engine.captureGuide(page, label: "hero") let shadowWasEnabled = try engine.block.isDropShadowEnabled(textBlock) try engine.block.setDropShadowEnabled(textBlock, enabled: false) let shadowAfterDisable = try engine.block.isDropShadowEnabled(textBlock) try engine.block.setDropShadowEnabled(textBlock, enabled: shadowWasEnabled) let shadowAfterRestore = try engine.block.isDropShadowEnabled(textBlock) print("Drop shadow toggled off then on: \(shadowAfterDisable) -> \(shadowAfterRestore)") let effects = try engine.block.getEffects(imageBlock) if let glowEffect = effects.first { try engine.block.setEffectEnabled(effectID: glowEffect, enabled: false) let glowAfterDisable = try engine.block.isEffectEnabled(effectID: glowEffect) try engine.block.setEffectEnabled(effectID: glowEffect, enabled: true) let glowAfterRestore = try engine.block.isEffectEnabled(effectID: glowEffect) print("Glow toggled off then on: \(glowAfterDisable) -> \(glowAfterRestore)") } let attachedEffects = try engine.block.getEffects(imageBlock) if let glowToRemove = attachedEffects.first { try engine.block.removeEffect(imageBlock, index: 0) try engine.block.destroy(glowToRemove) } } ``` Add visual depth and emphasis to design elements using drop shadows and glow effects with the CE.SDK Engine API. Drop shadows make elements appear to float above the canvas, while glow effects add luminous halos that draw attention. ![A page with a white title that casts a teal drop shadow, an image with a luminous glow, and a second image combining both a drop shadow and a glow.](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-shadows-and-glows) CE.SDK exposes two distinct approaches. **Drop shadows** are native block properties, configured directly on supported blocks with dedicated `setDropShadow*(_:)` methods. **Glow effects** are created through the effects system with `createEffect(_:)` and tuned with property setters. Drop shadows apply to graphic, text, and shape blocks. Glow effects apply to blocks that support effects, such as graphic and shape blocks; text blocks don't support effects. ## Using the Built-in UI On iOS, the CE.SDK editor provides no built-in controls for drop shadows, so configure them with the Engine APIs below. Glow effects can be exposed to users through the effects sheet, depending on which effect sources your editor configuration registers. On macOS and Mac Catalyst there is no editor UI, so use the Engine APIs directly. ## Drop Shadow Configuration Drop shadows are native block properties configured through dedicated API methods. ### Check Support and Enable Verify a block supports drop shadows with `supportsDropShadow(_:)` before configuring one. ```swift highlight-shadowsAndGlows-checkDropShadowSupport let supportsDropShadow = try engine.block.supportsDropShadow(textBlock) print("Block supports drop shadow: \(supportsDropShadow)") ``` Once verified, enable the shadow with `setDropShadowEnabled(_:enabled:)` and read the current state back with `isDropShadowEnabled(_:)`. ```swift highlight-shadowsAndGlows-enableDropShadow try engine.block.setDropShadowEnabled(textBlock, enabled: true) let shadowEnabled = try engine.block.isDropShadowEnabled(textBlock) print("Drop shadow enabled: \(shadowEnabled)") ``` ### Set Shadow Color Set the shadow color with `setDropShadowColor(_:color:)`, passing a `Color`. The color's alpha controls shadow opacity. `getDropShadowColor(_:)` returns the current color. ```swift highlight-shadowsAndGlows-setColor try engine.block.setDropShadowColor(textBlock, color: .rgba(r: 0.0, g: 0.3, b: 0.4, a: 0.8)) let shadowColor: Color = try engine.block.getDropShadowColor(textBlock) print("Drop shadow color: \(shadowColor)") ``` ### Set Shadow Position Control the horizontal and vertical offset with `setDropShadowOffsetX(_:offsetX:)` and `setDropShadowOffsetY(_:offsetY:)`. Positive values move the shadow right and down; negative values move it left and up. ```swift highlight-shadowsAndGlows-setOffset try engine.block.setDropShadowOffsetX(textBlock, offsetX: 6) try engine.block.setDropShadowOffsetY(textBlock, offsetY: 6) let offsetX = try engine.block.getDropShadowOffsetX(textBlock) let offsetY = try engine.block.getDropShadowOffsetY(textBlock) print("Drop shadow offset: \(offsetX), \(offsetY)") ``` ### Configure Blur Radius Set the shadow softness with `setDropShadowBlurRadiusX(_:blurRadiusX:)` and `setDropShadowBlurRadiusY(_:blurRadiusY:)`. Higher values produce softer, more diffuse edges. ```swift highlight-shadowsAndGlows-setBlur try engine.block.setDropShadowBlurRadiusX(textBlock, blurRadiusX: 12) try engine.block.setDropShadowBlurRadiusY(textBlock, blurRadiusY: 12) let blurX = try engine.block.getDropShadowBlurRadiusX(textBlock) let blurY = try engine.block.getDropShadowBlurRadiusY(textBlock) print("Drop shadow blur: \(blurX), \(blurY)") ``` ## Glow Effect Configuration Glow effects are created through the effects system and attached to blocks that support effects. ### Check Support and Create Glow Verify a block supports effects with `supportsEffects(_:)`. ```swift highlight-shadowsAndGlows-checkGlowSupport let supportsEffects = try engine.block.supportsEffects(imageBlock) print("Block supports effects: \(supportsEffects)") ``` Create the glow with `createEffect(.glow)` and attach it with `appendEffect(_:effectID:)`. ```swift highlight-shadowsAndGlows-createGlow let glow = try engine.block.createEffect(.glow) try engine.block.appendEffect(imageBlock, effectID: glow) ``` ### Configure Glow Parameters Tune the glow's appearance with `setFloat(_:property:value:)` using these properties: - `effect/glow/size` — The spread of the glow around the block. - `effect/glow/amount` — The intensity of the glow. - `effect/glow/darkness` — How dark the glow's falloff renders. ```swift highlight-shadowsAndGlows-configureGlow try engine.block.setFloat(glow, property: "effect/glow/size", value: 10) try engine.block.setFloat(glow, property: "effect/glow/amount", value: 0.7) try engine.block.setFloat(glow, property: "effect/glow/darkness", value: 0.25) ``` ## Combining Shadows and Glows A drop shadow and a glow can both apply to the same block. Drop shadows render independently of the effects stack, so a block can carry both at once for layered depth and emphasis. Because this block is a shape, `setDropShadowClip(_:clip:)` controls whether the shadow is clipped out of the shape's own area: `clip: true` keeps the shadow strictly outside the shape, while `clip: false` (the default, used here) lets the shadow render behind the block and show through wherever the fill is not fully opaque. Clipping applies to shape blocks only, and `getDropShadowClip(_:)` reads the setting back. ```swift highlight-shadowsAndGlows-combine if try engine.block.supportsDropShadow(combinedBlock) { try engine.block.setDropShadowEnabled(combinedBlock, enabled: true) try engine.block.setDropShadowColor(combinedBlock, color: .rgba(r: 0.0, g: 0.2, b: 0.3, a: 0.6)) try engine.block.setDropShadowOffsetX(combinedBlock, offsetX: 8) try engine.block.setDropShadowOffsetY(combinedBlock, offsetY: 8) try engine.block.setDropShadowBlurRadiusX(combinedBlock, blurRadiusX: 20) try engine.block.setDropShadowBlurRadiusY(combinedBlock, blurRadiusY: 20) try engine.block.setDropShadowClip(combinedBlock, clip: false) let clipsToShape = try engine.block.getDropShadowClip(combinedBlock) print("Drop shadow clips to shape: \(clipsToShape)") } if try engine.block.supportsEffects(combinedBlock) { let combinedGlow = try engine.block.createEffect(.glow) try engine.block.appendEffect(combinedBlock, effectID: combinedGlow) try engine.block.setFloat(combinedGlow, property: "effect/glow/size", value: 8) try engine.block.setFloat(combinedGlow, property: "effect/glow/amount", value: 0.5) try engine.block.setFloat(combinedGlow, property: "effect/glow/darkness", value: 0.15) } ``` ## Managing Shadow and Glow State ### Toggle Drop Shadows Show or hide a drop shadow with `setDropShadowEnabled(_:enabled:)` without discarding its color, offset, or blur. `isDropShadowEnabled(_:)` reports the current state. ```swift highlight-shadowsAndGlows-toggleShadow let shadowWasEnabled = try engine.block.isDropShadowEnabled(textBlock) try engine.block.setDropShadowEnabled(textBlock, enabled: false) let shadowAfterDisable = try engine.block.isDropShadowEnabled(textBlock) try engine.block.setDropShadowEnabled(textBlock, enabled: shadowWasEnabled) let shadowAfterRestore = try engine.block.isDropShadowEnabled(textBlock) print("Drop shadow toggled off then on: \(shadowAfterDisable) -> \(shadowAfterRestore)") ``` ### Toggle Glow Effects Show or hide a glow with `setEffectEnabled(effectID:enabled:)`, and query it with `isEffectEnabled(effectID:)`. Use `getEffects(_:)` to retrieve a block's attached effects. ```swift highlight-shadowsAndGlows-toggleGlow let effects = try engine.block.getEffects(imageBlock) if let glowEffect = effects.first { try engine.block.setEffectEnabled(effectID: glowEffect, enabled: false) let glowAfterDisable = try engine.block.isEffectEnabled(effectID: glowEffect) try engine.block.setEffectEnabled(effectID: glowEffect, enabled: true) let glowAfterRestore = try engine.block.isEffectEnabled(effectID: glowEffect) print("Glow toggled off then on: \(glowAfterDisable) -> \(glowAfterRestore)") } ``` ### Remove Glow Effects To remove a glow permanently, detach it from the block with `removeEffect(_:index:)`, then release the instance with `destroy(_:)`. ```swift highlight-shadowsAndGlows-removeGlow let attachedEffects = try engine.block.getEffects(imageBlock) if let glowToRemove = attachedEffects.first { try engine.block.removeEffect(imageBlock, index: 0) try engine.block.destroy(glowToRemove) } ``` ## Troubleshooting ### Shadow Not Visible - Confirm the block supports drop shadows with `supportsDropShadow(_:)`. - Confirm the shadow is enabled with `isDropShadowEnabled(_:)`. - Use a non-zero offset or blur radius so the shadow extends past the block. - Give the shadow color enough alpha to be visible. ### Glow Not Appearing - Confirm the block supports effects with `supportsEffects(_:)`. - Confirm the effect is enabled with `isEffectEnabled(effectID:)`. - Use non-zero `effect/glow/size` and `effect/glow/amount` values. ### Performance Considerations - Keep the number of effects per block small on lower-end devices. - Use moderate blur radius and glow size values to limit render cost. ## API Reference | Method | Description | | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | | `engine.block.supportsDropShadow(_:)` | Check if a block supports drop shadows | | `engine.block.setDropShadowEnabled(_:enabled:)` | Enable or disable the drop shadow | | `engine.block.isDropShadowEnabled(_:)` | Query whether the drop shadow is enabled | | `engine.block.setDropShadowColor(_:color:)` | Set the shadow color | | `engine.block.getDropShadowColor(_:)` | Get the current shadow color | | `engine.block.setDropShadowOffsetX(_:offsetX:)` / `engine.block.setDropShadowOffsetY(_:offsetY:)` | Set the horizontal and vertical offset | | `engine.block.getDropShadowOffsetX(_:)` / `engine.block.getDropShadowOffsetY(_:)` | Get the horizontal and vertical offset | | `engine.block.setDropShadowBlurRadiusX(_:blurRadiusX:)` / `engine.block.setDropShadowBlurRadiusY(_:blurRadiusY:)` | Set the horizontal and vertical blur radius | | `engine.block.getDropShadowBlurRadiusX(_:)` / `engine.block.getDropShadowBlurRadiusY(_:)` | Get the horizontal and vertical blur radius | | `engine.block.setDropShadowClip(_:clip:)` | Set shadow clipping (shape blocks only) | | `engine.block.getDropShadowClip(_:)` | Get the shadow clipping setting | | `engine.block.supportsEffects(_:)` | Check if a block supports effects | | `engine.block.createEffect(_:)` | Create an effect instance, such as `.glow` | | `engine.block.appendEffect(_:effectID:)` | Attach an effect to a block | | `engine.block.getEffects(_:)` | Get all effects on a block | | `engine.block.setFloat(_:property:value:)` | Set a glow parameter | | `engine.block.setEffectEnabled(effectID:enabled:)` | Enable or disable an effect | | `engine.block.isEffectEnabled(effectID:)` | Query whether an effect is enabled | | `engine.block.removeEffect(_:index:)` | Detach an effect from a block | | `engine.block.destroy(_:)` | Release an effect instance | ## Next Steps [Using Strokes](https://img.ly/docs/cesdk/mac-catalyst/outlines/strokes-c2e621/) - Add border outlines to elements [Apply a Filter or Effect](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) - Explore additional visual effects [Blur Effects](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/blur-71d642/) - Apply blur effects to 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: "Using Strokes" description: "Add and customize outlines around shapes, text, or images using stroke settings." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/outlines/strokes-c2e621/" --- > 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/) > [Outlines](https://img.ly/docs/cesdk/mac-catalyst/outlines-b7820c/) > [Using Strokes](https://img.ly/docs/cesdk/mac-catalyst/outlines/strokes-c2e621/) --- ```swift file=@cesdk_swift_examples/engine-guides-stroke/Stroke.swift reference-only import IMGLYEngine @MainActor func stroke(engine: Engine) async throws { // Demo scaffolding: a scene with a page and a single rectangle graphic block // to outline. A light fill keeps the rectangle visible so the blue stroke // stands out against it. 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)) let fill = try engine.block.getFill(block) try engine.block.setColor(fill, property: "fill/color/value", color: .rgba(r: 0.9, g: 0.9, b: 0.9, a: 1.0)) 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) guard try engine.block.supportsStroke(block) else { return } try engine.block.setStrokeEnabled(block, enabled: true) let strokeEnabled = try engine.block.isStrokeEnabled(block) print("Stroke enabled: \(strokeEnabled)") try engine.block.setStrokeColor(block, color: .rgba(r: 0.0, g: 0.0, b: 1.0, a: 1.0)) let strokeColor: Color = try engine.block.getStrokeColor(block) print("Stroke color: \(strokeColor)") try engine.block.setStrokeWidth(block, width: 10) let strokeWidth = try engine.block.getStrokeWidth(block) print("Stroke width: \(strokeWidth)") try await engine.captureGuide(page, label: "after-basic-stroke") try engine.block.setStrokeStyle(block, style: .dashed) let strokeStyle = try engine.block.getStrokeStyle(block) print("Stroke style is dashed: \(strokeStyle == .dashed)") try engine.block.setStrokePosition(block, position: .outer) let strokePosition = try engine.block.getStrokePosition(block) print("Stroke position is outer: \(strokePosition == .outer)") try engine.block.setStrokeCornerGeometry(block, cornerGeometry: .round) let strokeCornerGeometry = try engine.block.getStrokeCornerGeometry(block) print("Stroke corner geometry is round: \(strokeCornerGeometry == .round)") try await engine.captureGuide(page, label: "hero") } ``` Add outlines around shapes, text, and graphics to create emphasis, separation, or decorative effects. ![A centered rectangle with a blue, dashed, outer-positioned stroke and rounded corners](./assets/swift-based.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-stroke) Strokes are visual outlines on design blocks. This guide focuses on controlling stroke color, width, line pattern, position relative to the block edge, and corner geometry through the block API. Mutating stroke properties requires the `stroke/change` scope. Additional overprint, end-cap, and custom-dash APIs are listed in the API Reference. The snippets use a rectangle graphic block named `block`, but the same calls apply to any block once `supportsStroke()` returns `true`. ## Using the Built-in Stroke UI On iOS, the editor shows stroke controls in the Fill & Stroke sheet for selected blocks that support strokes and allow the `stroke/change` scope. That sheet can render fill controls, stroke controls, or both, depending on the selected block. The color row doubles as the on/off control: choosing no color disables the stroke, while choosing a swatch or custom color enables and colors it. - **No-color and color swatches** - Disable the stroke or apply a preset RGBA stroke color - **Color picker** - Choose a custom stroke color - **Width slider** - Adjust stroke thickness in design units - **Style picker** - Select solid, dashed, rounded-dashed, dotted, long-dashed, or long round-ended dashed patterns - **Position and corner pickers** - Adjust stroke placement and corner joins when available for the selected block ## Checking Stroke Support Before applying stroke settings, check whether the block supports them. Return before the mutation calls when `supportsStroke()` is `false`. ```swift highlight-stroke-checkSupport guard try engine.block.supportsStroke(block) else { return } ``` ## Enabling Strokes Enable the stroke with `setStrokeEnabled()`, then read the state back with `isStrokeEnabled()` when your app needs to update its UI or validate the change. ```swift highlight-stroke-enable try engine.block.setStrokeEnabled(block, enabled: true) let strokeEnabled = try engine.block.isStrokeEnabled(block) print("Stroke enabled: \(strokeEnabled)") ``` ## Setting Stroke Color Use `setStrokeColor()` with a `Color` value. The `.rgba` case takes red, green, blue, and alpha components from `0.0` to `1.0`. The sample sets a blue stroke and reads it back with `getStrokeColor()`. ```swift highlight-stroke-color try engine.block.setStrokeColor(block, color: .rgba(r: 0.0, g: 0.0, b: 1.0, a: 1.0)) let strokeColor: Color = try engine.block.getStrokeColor(block) print("Stroke color: \(strokeColor)") ``` 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 strokeColor: Color = ...`). ## Setting Stroke Width Set the thickness in design units with `setStrokeWidth()`. Larger values create more prominent outlines, and `getStrokeWidth()` returns the current value. ```swift highlight-stroke-width try engine.block.setStrokeWidth(block, width: 10) let strokeWidth = try engine.block.getStrokeWidth(block) print("Stroke width: \(strokeWidth)") ``` ## Stroke Styles Use `setStrokeStyle()` to control the line pattern. The `StrokeStyle` enum provides: - **`.solid`** - Continuous line - **`.dashed`** - Square-ended dashes with gaps - **`.dashedRound`** - Round-ended dashes with gaps - **`.dotted`** - Circular dots - **`.longDashed`** - Longer square-ended dashes - **`.longDashedRound`** - Longer round-ended dashes ```swift highlight-stroke-style try engine.block.setStrokeStyle(block, style: .dashed) let strokeStyle = try engine.block.getStrokeStyle(block) print("Stroke style is dashed: \(strokeStyle == .dashed)") ``` ## Stroke Position Use `setStrokePosition()` to control where the stroke renders relative to the block edge: - **`.center`** - Centered on the edge (default) - **`.inner`** - Rendered inside the block boundary - **`.outer`** - Rendered outside the block boundary Inner strokes stay within the block bounds, while outer strokes extend beyond them. ```swift highlight-stroke-position try engine.block.setStrokePosition(block, position: .outer) let strokePosition = try engine.block.getStrokePosition(block) print("Stroke position is outer: \(strokePosition == .outer)") ``` ## Stroke Corner Geometry Use `setStrokeCornerGeometry()` to control how stroke corners are joined. The effect is easiest to see on rectangular shapes. - **`.miter`** - Sharp pointed corners (default) - **`.round`** - Smoothly curved corners - **`.bevel`** - Flat cut corners ```swift highlight-stroke-corner try engine.block.setStrokeCornerGeometry(block, cornerGeometry: .round) let strokeCornerGeometry = try engine.block.getStrokeCornerGeometry(block) print("Stroke corner geometry is round: \(strokeCornerGeometry == .round)") ``` ## Troubleshooting If strokes do not appear as expected, check these common issues: - **Stroke not visible** - Verify `isStrokeEnabled()` returns `true` and the width is greater than `0`. - **Stroke color appears wrong** - Use normalized color components from `0.0` to `1.0`, not `0` to `255`. - **Stroke affects the visual bounds** - Use `.inner` to keep the stroke inside the block boundary, or `.outer` when the outline should extend beyond it. - **Block does not support strokes** - Guard mutations with `supportsStroke()` before applying stroke properties. ## API Reference | Method | Description | |--------|-------------| | `engine.block.supportsStroke(_:)` | Check whether a block supports strokes | | `engine.block.setStrokeEnabled(_:enabled:)` | Enable or disable the stroke | | `engine.block.isStrokeEnabled(_:)` | Check whether the stroke is enabled | | `engine.block.setStrokeColor(_:color:)` | Set the stroke color | | `engine.block.getStrokeColor(_:)` | Get the stroke color (annotate the return as `Color`) | | `engine.block.setStrokeWidth(_:width:)` | Set the stroke thickness in design units | | `engine.block.getStrokeWidth(_:)` | Get the current stroke width | | `engine.block.setStrokeStyle(_:style:)` | Set the stroke line pattern | | `engine.block.getStrokeStyle(_:)` | Get the current stroke style | | `engine.block.setStrokePosition(_:position:)` | Set the stroke position relative to the edge | | `engine.block.getStrokePosition(_:)` | Get the current stroke position | | `engine.block.setStrokeCornerGeometry(_:cornerGeometry:)` | Set the stroke corner join geometry | | `engine.block.getStrokeCornerGeometry(_:)` | Get the current stroke corner geometry | | `engine.block.setStrokeStartCap(_:cap:)` | Set the cap at the start of an open stroked path | | `engine.block.getStrokeStartCap(_:)` | Get the start cap of an open stroked path | | `engine.block.setStrokeEndCap(_:cap:)` | Set the cap at the end of an open stroked path | | `engine.block.getStrokeEndCap(_:)` | Get the end cap of an open stroked path | | `engine.block.setStrokeDashStartCap(_:cap:)` | Set the leading cap for each dash segment | | `engine.block.getStrokeDashStartCap(_:)` | Get the leading cap for each dash segment | | `engine.block.setStrokeDashEndCap(_:cap:)` | Set the trailing cap for each dash segment | | `engine.block.getStrokeDashEndCap(_:)` | Get the trailing cap for each dash segment | | `engine.block.setStrokeDashArray(_:dashArray:)` | Set a custom dash pattern in design units | | `engine.block.getStrokeDashArray(_:)` | Get the custom dash pattern | | `engine.block.setStrokeDashOffset(_:dashOffset:)` | Shift the custom dash pattern along the stroke | | `engine.block.getStrokeDashOffset(_:)` | Get the dash pattern offset | | `engine.block.setStrokeOverprint(_:overprint:)` | Mark eligible spot-color strokes for PDF overprint output | | `engine.block.getStrokeOverprint(_:)` | Check whether stroke overprint is enabled | ### Related Types | Type | Values | Description | |------|--------|-------------| | `StrokeStyle` | `.solid`, `.dashed`, `.dashedRound`, `.dotted`, `.longDashed`, `.longDashedRound` | Selects the preset line pattern | | `StrokePosition` | `.center`, `.inner`, `.outer` | Controls where the stroke is drawn relative to the block edge | | `StrokeCornerGeometry` | `.miter`, `.round`, `.bevel` | Controls how stroke corners join | | `StrokeCap` | `.butt`, `.round`, `.square` | Controls open-path and dash-segment end caps | ## Next Steps - [Apply Colors](https://img.ly/docs/cesdk/mac-catalyst/colors/apply-2211e3/) — Apply colors to design elements programmatically - [Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/) — Add solid colors, gradients, images, or videos inside blocks - [Shadows and Glows](https://img.ly/docs/cesdk/mac-catalyst/outlines/shadows-and-glows-6610fa/) — Add depth with shadow and glow effects - [Create and Edit Shapes](https://img.ly/docs/cesdk/mac-catalyst/insert-media/shapes-or-stickers-20ac68/) — Create shape blocks you can style with strokes --- ## 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: "Video Editor SDK" description: "Explore video editing features in CE.SDK including trimming, splitting, captions, and programmatic editing." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/overview-7d12d5/" --- > 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/) > [Editing Overview](https://img.ly/docs/cesdk/mac-catalyst/overview-7d12d5/) --- Use CreativeEditor SDK (CE.SDK) to build video editing experiences directly in your Apple app. CE.SDK supports both video and audio editing — including trimming, joining, adding text, annotating, and more — all performed on-device. Developers can integrate editing functionality using the CE.SDK editor UI or programmatically via the SDK API. CE.SDK also supports music and sound effects alongside video editing. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## Core Capabilities CreativeEditor SDK includes a comprehensive set of video editing tools, accessible through both a UI and a programmatic interface. Supported editing actions include: - **Trim, Split, Join, and Arrange**: Modify clips, reorder segments, and stitch together content. - **Transform**: Crop, rotate, resize, scale, and flip. - **Audio Editing**: Add, adjust, and synchronize audio including music, voiceovers, and effects. - **Programmatic Editing**: Control all editing features via API. CE.SDK is well-suited for scenarios like short-form content, reels, promotional videos, and other linear video workflows. ## Timeline Editor The [Timeline Editor](https://img.ly/docs/cesdk/mac-catalyst/create-video/timeline-editor-912252/) provides a familiar video editing experience for users. It supports: - Layered tracks for video and audio - Drag-and-drop sequencing with snapping - Trim handles, in/out points, and time offsets - Real-time preview updates ## Supported Input Formats and Codecs CE.SDK supports a wide range of video input formats and encodings, including: CE.SDK supports the most widely adopted video and audio codecs to ensure compatibility across platforms: ## Output and Export Options You can export edited videos in several formats, with control over resolution, encoding, and file size: ## UI-Based vs. Programmatic Editing CE.SDK offers a fully interactive editor with intuitive UI tools for creators. At the same time, developers can build workflows entirely programmatically using the SDK API. - Use the UI to let users trim, arrange, and caption videos manually - Use the API to automate the assembly or editing of videos at scale ## Customization You can tailor the editor to match your product's design and user needs: - Show or hide tools - Reorder UI elements and dock items - Apply custom themes, colors, or typography - Add additional plugin components ## Performance and File Size Considerations All editing operations are performed on-device. While this keeps user content private and the editor responsive, it introduces some limits: --- ## 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: "Improve Performance" description: "Optimize CE.SDK integration on Apple platforms with source sets, memory monitoring, export tuning, and lifecycle best practices." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/performance-3c12eb/" --- > 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/) > [Improve Performance](https://img.ly/docs/cesdk/mac-catalyst/performance-3c12eb/) --- ```swift file=@cesdk_swift_examples/engine-guides-performance/Performance.swift reference-only import Foundation import IMGLYEngine @MainActor func performance(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL try engine.editor.setSettingString( "basePath", value: baseURL.absoluteString, ) let scene = try engine.scene.create() try engine.scene.setDesignUnit(.px) 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 block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let imageFill = try engine.block.createFill(.image) try engine.block.setSourceSet(imageFill, property: "fill/image/sourceSet", sourceSet: [ .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-512x341.jpg"), width: 512, height: 341, ), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-883x589.jpg"), width: 883, height: 589, ), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1767x1178.jpg"), width: 1767, height: 1178, ), ]) try engine.block.setFill(block, fill: imageFill) try engine.block.appendChild(to: page, child: block) let usedMemory = try engine.editor.getUsedMemory() let availableMemory = try? engine.editor.getAvailableMemory() if let availableMemory { let total = usedMemory + availableMemory let usagePercentage = Double(usedMemory) / Double(total) * 100 print("Memory usage: \(usagePercentage)%") } let maxExportSize = try engine.editor.getMaxExportSize() 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 if !withinLimit { print("Page dimensions exceed the device export limit") } } let options = ExportOptions( jpegQuality: 0.8, targetWidth: 1280, targetHeight: 720, ) let blob = try await engine.block.export(page, mimeType: .jpeg, options: options) _ = blob } ``` Optimize CE.SDK integration for faster load times, efficient memory usage, and smooth runtime performance. CE.SDK ships a fully featured creative engine. Tuning how you load assets, manage memory, and configure exports keeps editing responsive on lower-end devices and keeps exports reliable on the entire fleet. This guide covers source sets for large assets, memory monitoring with the editor APIs, export size and quality tuning, and the engine initialization pattern. > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-performance) ## Managing Large Assets High-resolution images and videos consume significant memory. Use source sets to give the engine multiple resolution variants so it can pick the smallest one that still looks good at the current display size. ### Use Source Sets A source set is a list of `Source` entries with different resolutions for the same image or video. The engine picks the variant whose dimensions best match the current viewport and reaches for the higher-resolution entries only when needed for export. ```swift highlight-performance-sourceSets let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) let imageFill = try engine.block.createFill(.image) try engine.block.setSourceSet(imageFill, property: "fill/image/sourceSet", sourceSet: [ .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-512x341.jpg"), width: 512, height: 341, ), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-883x589.jpg"), width: 883, height: 589, ), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1767x1178.jpg"), width: 1767, height: 1178, ), ]) try engine.block.setFill(block, fill: imageFill) try engine.block.appendChild(to: page, child: block) ``` This reduces memory pressure during editing while preserving export quality. See [Source Sets](https://img.ly/docs/cesdk/mac-catalyst/import-media/source-sets-5679c8/) for the full API including video source sets and asset-source integration. ### Additional Optimization Tips - Remove unused blocks from the scene when no longer needed - Release the `Engine` reference when the editing session ends so ARC can reclaim its resources - Use efficient image formats (WebP, HEIF, optimized JPEG) for source assets ## Memory Management Use the editor's memory APIs to observe how much the engine currently holds and how much headroom remains. Track these values across long sessions to detect leaks or decide when to free unused assets. ```swift highlight-performance-memoryMonitoring let usedMemory = try engine.editor.getUsedMemory() let availableMemory = try? engine.editor.getAvailableMemory() if let availableMemory { let total = usedMemory + availableMemory let usagePercentage = Double(usedMemory) / Double(total) * 100 print("Memory usage: \(usagePercentage)%") } ``` Two notes about the Swift APIs: - `getUsedMemory()` and `getAvailableMemory()` both return byte counts as `Int64`. - `getAvailableMemory()` is unavailable on the iOS Simulator and throws there. Wrap the call in `try?` so the same code path works in unit tests and on real devices. ## Export Optimization Tune export resolution and quality to balance fidelity against time and memory. The settings below apply to image exports; video exports take their own dedicated options. ### Optimize Export Settings `ExportOptions` controls compression and downscaling. Lowering `targetWidth` / `targetHeight` and `jpegQuality` produces smaller files faster, at the cost of fidelity. ```swift highlight-performance-exportSettings let options = ExportOptions( jpegQuality: 0.8, targetWidth: 1280, targetHeight: 720, ) let blob = try await engine.block.export(page, mimeType: .jpeg, options: options) ``` | Property | Type | Purpose | | --- | --- | --- | | `targetWidth` / `targetHeight` | `Float` | Optional output dimensions in pixels. The block is rendered large enough to fill the target while keeping its aspect ratio. Leave at `0` to use the block's intrinsic size. | | `jpegQuality` | `Float` | JPEG quality in the range `(0, 1]`. Lower values trade quality for smaller files. Defaults to `0.9`. | | `pngCompressionLevel` | `Int` | PNG compression `0–9`. Higher values produce smaller files but take longer to encode. Defaults to `5`. | | `webpQuality` | `Float` | WebP quality in the range `(0, 1]`. Defaults to `1.0`. | ### Export Size Limits Different devices support different maximum export sizes. Call `getMaxExportSize()` to read the device's upper bound in pixels and validate page dimensions before kicking off a large export. ```swift highlight-performance-maxExportSize let maxExportSize = try engine.editor.getMaxExportSize() 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 if !withinLimit { print("Page dimensions exceed the device export limit") } } ``` `getWidth(_:)` and `getHeight(_:)` return values in design units, so the comparison only makes sense when the scene's design unit is `.px` AND the block's width and height modes are `.absolute`. With `.percent` or `.auto` modes, the returned value is relative to the parent or derived from content, not a pixel count. The returned value is a hard upper bound — exports can still fail for memory or other reasons. When the limit is unknown, the engine returns `Int32.max`. See [Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/) for the full pattern including `maxImageSize` tuning and recovery on export failure. ## Engine Lifecycle and Asset Loading Create the `Engine` once per editing session with `try await Engine(license:)` on `@MainActor`, then hold a strong reference for the lifetime of that session. ARC frees the engine's GPU resources, textures, and native buffers when the last reference goes away, so releasing the editor screen is enough to reclaim memory. Before loading any scene, point the engine at the asset base URL and register the default asset sources: ```swift highlight-performance-initialization try engine.editor.setSettingString( "basePath", value: baseURL.absoluteString, ) ``` `setSettingString("basePath", value:)` tells the engine where to fetch default assets, fonts, and shaders. Registering the default asset sources with `engine.asset.addLocalAssetSourceFromJSON(...)` makes the bundled images, audio, video, typefaces, and shapes searchable so the UI and the engine can resolve asset IDs at runtime. For production deployments, host these assets on your own infrastructure to improve reliability and remove the dependency on an external CDN, then point `basePath` at the asset location you control. See [Serve Assets From Your Server](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) for the full self-hosting pattern and [Architecture](https://img.ly/docs/cesdk/mac-catalyst/concepts/architecture-6ea9b2/) for the high-level component diagram and `basePath` rationale. ## Troubleshooting ### Memory warnings or crashes Monitor memory with `getUsedMemory()` and `getAvailableMemory()` and react when usage climbs. Common remediations: remove unused blocks, lower the `maxImageSize` setting, or release the `Engine` reference and recreate it for a fresh editing session. ### Export hangs or fails Validate page dimensions against `getMaxExportSize()` before exporting and lower `targetWidth` / `targetHeight` for large designs. For persistent failures, reduce `maxImageSize` so newly loaded textures stay within memory budgets. ### Slow asset loading Use a CDN-hosted `basePath` or pre-cache asset files alongside your app bundle. Source sets help here too — initial editing loads only the low-resolution entries. ## API Reference | Method | Description | | --- | --- | | `Engine(context:audioContext:license:userID:)` | Initialize a new engine instance | | `engine.editor.setSettingString("basePath", value:)` | Configure where engine assets are loaded from | | `engine.editor.getUsedMemory()` | Get current engine memory usage in bytes | | `engine.editor.getAvailableMemory()` | Get remaining available memory in bytes (throws on iOS Simulator) | | `engine.editor.getMaxExportSize()` | Get the maximum export edge length in pixels | | `engine.block.setSourceSet(_:property:sourceSet:)` | Provide multiple resolutions of an image or video | | `engine.block.export(_:mimeType:options:)` | Export a block with configurable size and quality | ## Next Steps - [Architecture](https://img.ly/docs/cesdk/mac-catalyst/concepts/architecture-6ea9b2/) — Understand CE.SDK structure and components - [Export Overview](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/overview-9ed3a8/) — Learn about export formats and options - [Size Limits](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/size-limits-6f0695/) — Configure limits on exported file dimensions and 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: "Solutions" description: "Production-ready editor configurations for CE.SDK" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/prebuilt-solutions-d0ed07/" --- > 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:** [Solutions](https://img.ly/docs/cesdk/mac-catalyst/prebuilt-solutions-d0ed07/) --- Choose a starter kit to get up and running quickly with CE.SDK. Each kit provides a complete, customizable editor configuration. ## Choosing the Right Starter Kit Each starter kit is optimized for a specific workflow. Pick the one that matches your use case: ### Photo Editing Use **Photo Editor** when your users need to edit single images—crop, apply filters, adjust colors, or remove backgrounds. Ideal for profile photo uploads, product image editing, or any workflow where users enhance one image at a time. ### Graphic Design Use **Design Editor** when your users create graphics with multiple elements—social media posts, marketing materials, or personalized templates. Supports text, images, shapes, and multi-page documents like presentations or brochures. For power users who need full creative control, **Design Editor (Advanced)** adds a comprehensive toolbar, layer management, and professional design tools. ### Video Production Use **Video Editor** when your users need to edit video content—trim clips, add effects, overlay text, and export to MP4. Perfect for social media videos, short-form content, or basic video editing workflows. For professional video production with multi-track timelines, transitions, and audio mixing, choose **Video Editor (Advanced)**. ### Read-Only Display Use **Design Viewer** or **Video Player** when you need to display content without editing capabilities. These lightweight kits are ideal for approval workflows, content previews, or embedding finished designs in your application. --- ## 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: "Rules" description: "Define and enforce layout, branding, and safety rules to ensure consistent and compliant designs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/rules-1427c0/" --- > 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/) > [Rules](https://img.ly/docs/cesdk/mac-catalyst/rules-1427c0/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/rules/overview-e27832/) - Define and enforce layout, branding, and safety rules to ensure consistent and compliant designs. - [Enforce Brand Guidelines](https://img.ly/docs/cesdk/mac-catalyst/rules/enforce-brand-guidelines-23a1e3/) - Learn how to restrict users to approved brand assets and prevent unauthorized modifications to brand elements - [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. - [Moderate Content](https://img.ly/docs/cesdk/mac-catalyst/rules/moderate-content-d5ff7e/) - Extract images and text from CE.SDK designs with the Swift Engine API, then integrate third-party moderation services to detect inappropriate content. --- ## 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: "Enforce Brand Guidelines" description: "Learn how to restrict users to approved brand assets and prevent unauthorized modifications to brand elements" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/rules/enforce-brand-guidelines-23a1e3/" --- > 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/) > [Rules](https://img.ly/docs/cesdk/mac-catalyst/rules-1427c0/) > [Enforce Brand Guidelines](https://img.ly/docs/cesdk/mac-catalyst/rules/enforce-brand-guidelines-23a1e3/) --- ```swift file=@cesdk_swift_examples/engine-guides-enforce-brand-guidelines/EnforceBrandGuidelines.swift reference-only import Foundation import IMGLYEngine @MainActor func enforceBrandGuidelines(engine: Engine) async throws { // Demo scaffolding: create the design scene and page that hosts the brand // template. In your app this is whatever scene the user is editing. 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) let pageWidth = try engine.block.getWidth(page) let pageHeight = try engine.block.getHeight(page) // Demo scaffolding: a base URL for the example font files. Point the font // URLs below at your own brand font files instead. let fontBaseURL = try engine.guidesBaseURL try engine.asset.addLocalSource(sourceID: "ly.img.typeface") try engine.asset.addAsset(to: "ly.img.typeface", asset: AssetDefinition( id: "brand-sans", payload: AssetPayload(typeface: Typeface(name: "Brand Sans", fonts: [ Font( uri: fontBaseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), Font( uri: fontBaseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ), ])), label: ["en": "Brand Sans"], )) 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) try engine.editor.setGlobalScope(key: "fill/changeType", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/destroy", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/duplicate", value: .defer) try engine.editor.setGlobalScope(key: "text/edit", value: .defer) try engine.editor.setGlobalScope(key: "text/character", value: .defer) let logoBlock = try engine.block.create(.graphic) try engine.block.setShape(logoBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(logoBlock, value: 200) try engine.block.setHeight(logoBlock, value: 80) try engine.block.setPositionX(logoBlock, value: 40) try engine.block.setPositionY(logoBlock, value: 40) let logoFill = try engine.block.createFill(.color) try engine.block.setColor(logoFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.8, a: 1.0)) try engine.block.setFill(logoBlock, fill: logoFill) try engine.block.setName(logoBlock, name: "Company Logo") try engine.block.appendChild(to: page, child: logoBlock) try engine.block.setScopeEnabled(logoBlock, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(logoBlock, key: "layer/resize", enabled: false) try engine.block.setScopeEnabled(logoBlock, key: "fill/change", enabled: false) try engine.block.setScopeEnabled(logoBlock, key: "fill/changeType", enabled: false) try engine.block.setScopeEnabled(logoBlock, key: "lifecycle/destroy", enabled: false) try engine.block.setScopeEnabled(logoBlock, key: "lifecycle/duplicate", enabled: false) let legalText = try engine.block.create(.text) try engine.block.setWidth(legalText, value: pageWidth - 80) try engine.block.setHeight(legalText, value: 30) try engine.block.setPositionX(legalText, value: 40) try engine.block.setPositionY(legalText, value: pageHeight - 50) try engine.block.replaceText(legalText, text: "© 2024 Company Name. All rights reserved.") try engine.block.setFloat(legalText, property: "text/fontSize", value: 36) try engine.block.setName(legalText, name: "Legal Text") try engine.block.appendChild(to: page, child: legalText) try engine.block.setScopeEnabled(legalText, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(legalText, key: "layer/resize", enabled: false) try engine.block.setScopeEnabled(legalText, key: "text/edit", enabled: false) try engine.block.setScopeEnabled(legalText, key: "text/character", enabled: false) try engine.block.setScopeEnabled(legalText, key: "lifecycle/destroy", enabled: false) try engine.block.setScopeEnabled(legalText, key: "lifecycle/duplicate", enabled: false) let contentBlock = try engine.block.create(.graphic) try engine.block.setShape(contentBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(contentBlock, value: 400) try engine.block.setHeight(contentBlock, value: 300) try engine.block.setPositionX(contentBlock, value: (pageWidth - 400) / 2) try engine.block.setPositionY(contentBlock, value: (pageHeight - 300) / 2) let contentFill = try engine.block.createFill(.color) try engine.block.setColor(contentFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.6, b: 0.0, a: 1.0)) try engine.block.setFill(contentBlock, fill: contentFill) try engine.block.setName(contentBlock, name: "Editable Content") try engine.block.appendChild(to: page, child: contentBlock) try engine.block.setScopeEnabled(contentBlock, key: "layer/move", enabled: true) try engine.block.setScopeEnabled(contentBlock, key: "layer/resize", enabled: true) try engine.block.setScopeEnabled(contentBlock, key: "fill/change", enabled: true) try engine.block.setScopeEnabled(contentBlock, key: "fill/changeType", enabled: true) try engine.block.setScopeEnabled(contentBlock, key: "lifecycle/destroy", enabled: true) try engine.block.setScopeEnabled(contentBlock, key: "lifecycle/duplicate", enabled: true) let editableText = try engine.block.create(.text) try engine.block.setWidth(editableText, value: 300) try engine.block.setHeight(editableText, value: 60) try engine.block.setPositionX(editableText, value: (pageWidth - 300) / 2) try engine.block.setPositionY(editableText, value: 150) try engine.block.replaceText(editableText, text: "Edit This Headline") try engine.block.setFloat(editableText, property: "text/fontSize", value: 64) try engine.block.setEnum(editableText, property: "text/horizontalAlignment", value: "Center") try engine.block.setName(editableText, name: "Editable Headline") try engine.block.appendChild(to: page, child: editableText) try engine.block.setScopeEnabled(editableText, key: "layer/move", enabled: true) try engine.block.setScopeEnabled(editableText, key: "layer/resize", enabled: true) try engine.block.setScopeEnabled(editableText, key: "text/edit", enabled: true) try engine.block.setScopeEnabled(editableText, key: "text/character", enabled: true) try engine.block.setScopeEnabled(editableText, key: "lifecycle/destroy", enabled: true) let canMoveLogo = try engine.block.isAllowedByScope(logoBlock, key: "layer/move") let canEditLegal = try engine.block.isAllowedByScope(legalText, key: "text/edit") let canEditContent = try engine.block.isAllowedByScope(contentBlock, key: "fill/change") print("Logo is locked:", !canMoveLogo) // true print("Legal text is locked:", !canEditLegal) // true print("Content block is editable:", canEditContent) // true let blob = try await engine.block.export(page, mimeType: .png) let outputURL = FileManager.default.temporaryDirectory .appendingPathComponent("enforce-brand-guidelines-result.png") try blob.write(to: outputURL) } ``` Learn how to restrict the available fonts to brand typefaces and lock brand elements like logos and legal text from modification, while keeping the rest of a design fully editable. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-enforce-brand-guidelines) Brand guidelines enforcement in CE.SDK combines two complementary approaches: restricting which assets users can choose and controlling what editing operations are permitted on brand elements. This guide restricts the available fonts to an approved set and uses the scopes system to lock brand elements like logos and legal text so they cannot be modified. On iOS, to restrict the colors users can pick in the editor, configure the editor's color palette — see the [Color Palette](#broken-link-429fd9) guide. The example builds on a scene with a single page; adapt the block creation to the scene your app edits. ## Restricting Fonts to Brand Typefaces Register the `ly.img.typeface` asset source with only your approved typefaces — instead of loading the default typeface source — so only brand fonts are available to choose from. On iOS, the editor's font picker reads its typefaces from this source, so it then offers only the registered brand fonts. ```swift highlight-enforceBrand-restrictFonts try engine.asset.addLocalSource(sourceID: "ly.img.typeface") try engine.asset.addAsset(to: "ly.img.typeface", asset: AssetDefinition( id: "brand-sans", payload: AssetPayload(typeface: Typeface(name: "Brand Sans", fonts: [ Font( uri: fontBaseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), Font( uri: fontBaseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ), ])), label: ["en": "Brand Sans"], )) ``` Each typeface has a name and a list of `Font` entries; every font carries a file URL, a subfamily name, a `FontWeight`, and a `FontStyle`. Point the font URLs at your own brand font files. ## Setting Global Scopes to Defer Scopes control which operations are permitted. Setting a global scope to `.defer` hands the decision to each block, so per-block settings take effect. Without this, the global value (`.allow` or `.deny`) applies everywhere and block-level settings are ignored. ```swift highlight-enforceBrand-globalScopeDefer 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) try engine.editor.setGlobalScope(key: "fill/changeType", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/destroy", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/duplicate", value: .defer) try engine.editor.setGlobalScope(key: "text/edit", value: .defer) try engine.editor.setGlobalScope(key: "text/character", value: .defer) ``` ## Creating and Locking Brand Elements ### Creating a Logo Block Create a brand element that represents the company logo and give it a fixed position, size, and brand color fill. ```swift highlight-enforceBrand-createLogo let logoBlock = try engine.block.create(.graphic) try engine.block.setShape(logoBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(logoBlock, value: 200) try engine.block.setHeight(logoBlock, value: 80) try engine.block.setPositionX(logoBlock, value: 40) try engine.block.setPositionY(logoBlock, value: 40) let logoFill = try engine.block.createFill(.color) try engine.block.setColor(logoFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.8, a: 1.0)) try engine.block.setFill(logoBlock, fill: logoFill) try engine.block.setName(logoBlock, name: "Company Logo") try engine.block.appendChild(to: page, child: logoBlock) ``` ### Locking the Logo Disable the relevant scopes on the logo so it cannot be moved, resized, recolored, duplicated, or deleted. ```swift highlight-enforceBrand-lockLogo try engine.block.setScopeEnabled(logoBlock, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(logoBlock, key: "layer/resize", enabled: false) try engine.block.setScopeEnabled(logoBlock, key: "fill/change", enabled: false) try engine.block.setScopeEnabled(logoBlock, key: "fill/changeType", enabled: false) try engine.block.setScopeEnabled(logoBlock, key: "lifecycle/destroy", enabled: false) try engine.block.setScopeEnabled(logoBlock, key: "lifecycle/duplicate", enabled: false) ``` ### Locking Legal Text Create the legally required text and lock it the same way, additionally disabling `text/edit` so its wording cannot change, `text/character` so its font and styling stay fixed, and `lifecycle/duplicate` so it cannot be copied. ```swift highlight-enforceBrand-createLegalText let legalText = try engine.block.create(.text) try engine.block.setWidth(legalText, value: pageWidth - 80) try engine.block.setHeight(legalText, value: 30) try engine.block.setPositionX(legalText, value: 40) try engine.block.setPositionY(legalText, value: pageHeight - 50) try engine.block.replaceText(legalText, text: "© 2024 Company Name. All rights reserved.") try engine.block.setFloat(legalText, property: "text/fontSize", value: 36) try engine.block.setName(legalText, name: "Legal Text") try engine.block.appendChild(to: page, child: legalText) try engine.block.setScopeEnabled(legalText, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(legalText, key: "layer/resize", enabled: false) try engine.block.setScopeEnabled(legalText, key: "text/edit", enabled: false) try engine.block.setScopeEnabled(legalText, key: "text/character", enabled: false) try engine.block.setScopeEnabled(legalText, key: "lifecycle/destroy", enabled: false) try engine.block.setScopeEnabled(legalText, key: "lifecycle/duplicate", enabled: false) ``` ## Creating Editable Content Areas While brand elements stay locked, other blocks can remain fully editable. Enable the scopes you want users to control on each editable block. ```swift highlight-enforceBrand-createEditableContent let contentBlock = try engine.block.create(.graphic) try engine.block.setShape(contentBlock, shape: engine.block.createShape(.rect)) try engine.block.setWidth(contentBlock, value: 400) try engine.block.setHeight(contentBlock, value: 300) try engine.block.setPositionX(contentBlock, value: (pageWidth - 400) / 2) try engine.block.setPositionY(contentBlock, value: (pageHeight - 300) / 2) let contentFill = try engine.block.createFill(.color) try engine.block.setColor(contentFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.6, b: 0.0, a: 1.0)) try engine.block.setFill(contentBlock, fill: contentFill) try engine.block.setName(contentBlock, name: "Editable Content") try engine.block.appendChild(to: page, child: contentBlock) try engine.block.setScopeEnabled(contentBlock, key: "layer/move", enabled: true) try engine.block.setScopeEnabled(contentBlock, key: "layer/resize", enabled: true) try engine.block.setScopeEnabled(contentBlock, key: "fill/change", enabled: true) try engine.block.setScopeEnabled(contentBlock, key: "fill/changeType", enabled: true) try engine.block.setScopeEnabled(contentBlock, key: "lifecycle/destroy", enabled: true) try engine.block.setScopeEnabled(contentBlock, key: "lifecycle/duplicate", enabled: true) ``` For text that should be editable, also enable `text/character` so users can restyle its font, style, and alignment: ```swift highlight-enforceBrand-createEditableText let editableText = try engine.block.create(.text) try engine.block.setWidth(editableText, value: 300) try engine.block.setHeight(editableText, value: 60) try engine.block.setPositionX(editableText, value: (pageWidth - 300) / 2) try engine.block.setPositionY(editableText, value: 150) try engine.block.replaceText(editableText, text: "Edit This Headline") try engine.block.setFloat(editableText, property: "text/fontSize", value: 64) try engine.block.setEnum(editableText, property: "text/horizontalAlignment", value: "Center") try engine.block.setName(editableText, name: "Editable Headline") try engine.block.appendChild(to: page, child: editableText) try engine.block.setScopeEnabled(editableText, key: "layer/move", enabled: true) try engine.block.setScopeEnabled(editableText, key: "layer/resize", enabled: true) try engine.block.setScopeEnabled(editableText, key: "text/edit", enabled: true) try engine.block.setScopeEnabled(editableText, key: "text/character", enabled: true) try engine.block.setScopeEnabled(editableText, key: "lifecycle/destroy", enabled: true) ``` ## Validating Brand Compliance Confirm that the constraints are enforced with `engine.block.isAllowedByScope(_:key:)`, which considers both the global and block-level scope settings. ```swift highlight-enforceBrand-validateCompliance let canMoveLogo = try engine.block.isAllowedByScope(logoBlock, key: "layer/move") let canEditLegal = try engine.block.isAllowedByScope(legalText, key: "text/edit") let canEditContent = try engine.block.isAllowedByScope(contentBlock, key: "fill/change") print("Logo is locked:", !canMoveLogo) // true print("Legal text is locked:", !canEditLegal) // true print("Content block is editable:", canEditContent) // true ``` ## Exporting the Result Export the page with the brand guidelines applied. The locked blocks remain part of the design and render alongside the editable content. ```swift highlight-enforceBrand-export let blob = try await engine.block.export(page, mimeType: .png) let outputURL = FileManager.default.temporaryDirectory .appendingPathComponent("enforce-brand-guidelines-result.png") try blob.write(to: outputURL) ``` ## Troubleshooting - **Locked elements still movable**: Make sure the global scope is set to `.defer` before changing block-level settings — block-level values are ignored while the global scope is `.allow` or `.deny`. - **Brand elements still editable**: Confirm the matching scope (for example `lifecycle/destroy` or `text/edit`) is disabled on the specific block. - **Validation always passes**: `isAllowedByScope(_:key:)` reflects the global scope unless it is `.defer`; verify the global scope before relying on block-level results. ## API Reference | Method | Category | Purpose | |--------|----------|---------| | `engine.asset.addLocalSource(sourceID:)` | Asset | Create or register an asset source by ID | | `engine.asset.addAsset(to:asset:)` | Asset | Add an asset (such as a brand typeface) to a source | | `engine.editor.setGlobalScope(key:value:)` | Scope | Set an editor-wide scope to `.defer` for block-level control | | `engine.block.setScopeEnabled(_:key:enabled:)` | Scope | Enable or disable a scope for a specific block | | `engine.block.isAllowedByScope(_:key:)` | Scope | Check whether an operation is allowed | | `engine.block.export(_:mimeType:)` | Block | Export the design with brand guidelines applied | --- ## 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 Content" description: "Lock design elements to prevent unwanted modifications using CE.SDK's scope-based permission system." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/rules/lock-content-9fa727/" --- > 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/) > [Rules](https://img.ly/docs/cesdk/mac-catalyst/rules-1427c0/) > [Lock Content](https://img.ly/docs/cesdk/mac-catalyst/rules/lock-content-9fa727/) --- ```swift file=@cesdk_swift_examples/engine-guides-lock-content/LockContent.swift reference-only import Foundation import IMGLYEngine @MainActor func lockContent(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 scene with four blocks, each demonstrating a different // locking outcome. This setup runs before any scope is locked, 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) // Top-left: an image that stays fully locked. let lockedImage = try engine.block.create(.graphic) try engine.block.setShape(lockedImage, shape: engine.block.createShape(.rect)) let lockedFill = try engine.block.createFill(.image) try engine.block.setURL(lockedFill, property: "fill/image/imageFileURI", value: sampleImage1) try engine.block.setFill(lockedImage, fill: lockedFill) try engine.block.setPositionX(lockedImage, value: 185) try engine.block.setPositionY(lockedImage, value: 70) try engine.block.setWidth(lockedImage, value: 300) try engine.block.setHeight(lockedImage, value: 200) try engine.block.setName(lockedImage, name: "Locked Image") try engine.block.appendChild(to: page, child: lockedImage) // Top-right: a text block that allows text editing only. let editableText = try engine.block.create(.text) try engine.block.setString(editableText, property: "text/text", value: "Edit me!") try engine.block.setFloat(editableText, property: "text/fontSize", value: 90) try engine.block.setPositionX(editableText, value: 565) try engine.block.setPositionY(editableText, value: 70) try engine.block.setWidth(editableText, value: 450) try engine.block.setHeight(editableText, value: 200) try engine.block.setName(editableText, name: "Editable Text") try engine.block.appendChild(to: page, child: editableText) // Bottom-left: an image that allows replacement only. let replaceableImage = try engine.block.create(.graphic) try engine.block.setShape(replaceableImage, shape: engine.block.createShape(.rect)) let replaceableFill = try engine.block.createFill(.image) try engine.block.setURL(replaceableFill, property: "fill/image/imageFileURI", value: sampleImage2) try engine.block.setFill(replaceableImage, fill: replaceableFill) try engine.block.setPositionX(replaceableImage, value: 185) try engine.block.setPositionY(replaceableImage, value: 380) try engine.block.setWidth(replaceableImage, value: 300) try engine.block.setHeight(replaceableImage, value: 200) try engine.block.setName(replaceableImage, name: "Replaceable Image") try engine.block.appendChild(to: page, child: replaceableImage) // Bottom-right: a shape that allows moving and resizing only. let movableShape = try engine.block.create(.graphic) try engine.block.setShape(movableShape, shape: engine.block.createShape(.rect)) let shapeFill = try engine.block.createFill(.color) try engine.block.setColor(shapeFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.6, b: 0.9, a: 1.0)) try engine.block.setFill(movableShape, fill: shapeFill) try engine.block.setPositionX(movableShape, value: 565) try engine.block.setPositionY(movableShape, value: 380) try engine.block.setWidth(movableShape, value: 200) try engine.block.setHeight(movableShape, value: 200) try engine.block.setName(movableShape, name: "Movable Shape") try engine.block.appendChild(to: page, child: movableShape) let allScopes = engine.editor.findAllScopes() print("Available scopes:", allScopes) for scope in allScopes { try engine.editor.setGlobalScope(key: scope, value: .deny) } // editor/select was locked along with everything else. Re-open it so the // interactive blocks below can be selected; a block cannot be touched at all // while its selection is denied, no matter which other scopes are enabled. try engine.editor.setGlobalScope(key: "editor/select", value: .defer) try engine.block.setScopeEnabled(editableText, key: "editor/select", enabled: true) try engine.block.setScopeEnabled(replaceableImage, key: "editor/select", enabled: true) try engine.block.setScopeEnabled(movableShape, key: "editor/select", enabled: true) // text/edit gates the content, text/character gates styling (font, size, color). try engine.editor.setGlobalScope(key: "text/edit", value: .defer) try engine.editor.setGlobalScope(key: "text/character", value: .defer) try engine.block.setScopeEnabled(editableText, key: "text/edit", enabled: true) try engine.block.setScopeEnabled(editableText, key: "text/character", enabled: true) try engine.editor.setGlobalScope(key: "fill/change", value: .defer) try engine.block.setScopeEnabled(replaceableImage, 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.block.setScopeEnabled(movableShape, key: "layer/move", enabled: true) try engine.block.setScopeEnabled(movableShape, key: "layer/resize", enabled: true) let canEditText = try engine.block.isAllowedByScope(editableText, key: "text/edit") let canMoveLockedImage = try engine.block.isAllowedByScope(lockedImage, key: "layer/move") let canReplaceImage = try engine.block.isAllowedByScope(replaceableImage, key: "fill/change") let canMoveShape = try engine.block.isAllowedByScope(movableShape, key: "layer/move") print("Can edit text:", canEditText) // true print("Can move locked image:", canMoveLockedImage) // false print("Can replace image:", canReplaceImage) // true print("Can move shape:", canMoveShape) // true let textEditGlobal = try engine.editor.getGlobalScope(key: "text/edit") let textEditEnabled = try engine.block.isScopeEnabled(editableText, key: "text/edit") print("Global text/edit is .defer:", textEditGlobal == .defer) // true print("Block-level text/edit enabled:", textEditEnabled) // true } ``` Lock design elements to prevent unwanted modifications using CE.SDK's scope-based permission system. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-lock-content) CE.SDK uses **scopes** to control what users can modify in a design. Each scope gates a specific capability—moving, resizing, text editing, image replacement, and more. The permission system has two layers: **global scopes** set defaults for the entire scene, and **block-level scopes** override those defaults when the global scope is set to `.defer`. The example builds a sample page with four blocks—a fully locked image, an editable text block, a replaceable image, and a movable shape—then applies a different permission policy to each. This guide covers how to discover available scopes, lock an entire design, and selectively enable specific editing capabilities on individual blocks. ## Understanding the Scope Permission Model Global and block-level scopes combine to determine whether an operation is permitted. The global scope can be set to one of three values: | Global Scope | Block Scope | Result | | ------------ | ----------- | --------- | | `.allow` | any | Permitted | | `.deny` | any | Blocked | | `.defer` | enabled | Permitted | | `.defer` | disabled | Blocked | When global is `.allow`, the operation is always permitted regardless of block settings. When global is `.deny`, it is always blocked. When global is `.defer`, the block-level enabled state determines the outcome. ## Discovering Available Scopes Retrieve every available scope name with `engine.editor.findAllScopes()`. It returns an array of scope identifiers you can pass to the global and block-level scope APIs. ```swift highlight-lockContent-findAllScopes let allScopes = engine.editor.findAllScopes() print("Available scopes:", allScopes) ``` ## Locking an Entire Design To lock everything, iterate through all scopes and set each global scope to `.deny`. This blocks every editing operation on every block in the design. ```swift highlight-lockContent-lockAll for scope in allScopes { try engine.editor.setGlobalScope(key: scope, value: .deny) } ``` When all scopes are set to `.deny`, the `editor/select` scope is locked too. Users cannot interact with a block they cannot select, so before enabling specific capabilities you must also set `editor/select` to `.defer` and enable it on the blocks users should be able to reach. ## Selective Locking Patterns In most real-world scenarios you want to lock some aspects while allowing others. The following patterns enable specific capabilities on individual blocks after everything has been locked. ### Allowing Text Editing To let users edit text content but nothing else, set the `text/edit` and `text/character` global scopes to `.defer`, then enable them on specific text blocks with `engine.block.setScopeEnabled(_:key:enabled:)`. ```swift highlight-lockContent-textEdit // text/edit gates the content, text/character gates styling (font, size, color). try engine.editor.setGlobalScope(key: "text/edit", value: .defer) try engine.editor.setGlobalScope(key: "text/character", value: .defer) try engine.block.setScopeEnabled(editableText, key: "text/edit", enabled: true) try engine.block.setScopeEnabled(editableText, key: "text/character", enabled: true) ``` ### Allowing Image Replacement To let users swap images while protecting layout, set the `fill/change` global scope to `.defer` and enable it on specific image blocks. ```swift highlight-lockContent-imageReplace try engine.editor.setGlobalScope(key: "fill/change", value: .defer) try engine.block.setScopeEnabled(replaceableImage, key: "fill/change", enabled: true) ``` ### Allowing Position Adjustments To allow repositioning and resizing of specific elements, set `layer/move` and `layer/resize` to `.defer` globally, then enable them on the chosen blocks. ```swift highlight-lockContent-positionAdjust try engine.editor.setGlobalScope(key: "layer/move", value: .defer) try engine.editor.setGlobalScope(key: "layer/resize", value: .defer) try engine.block.setScopeEnabled(movableShape, key: "layer/move", enabled: true) try engine.block.setScopeEnabled(movableShape, key: "layer/resize", enabled: true) ``` ## Checking Permissions Verify the effective permission on a block with `engine.block.isAllowedByScope(_:key:)`. It returns `true` when the operation is permitted after evaluating both the global and block-level settings. ```swift highlight-lockContent-checkPermissions let canEditText = try engine.block.isAllowedByScope(editableText, key: "text/edit") let canMoveLockedImage = try engine.block.isAllowedByScope(lockedImage, key: "layer/move") let canReplaceImage = try engine.block.isAllowedByScope(replaceableImage, key: "fill/change") let canMoveShape = try engine.block.isAllowedByScope(movableShape, key: "layer/move") print("Can edit text:", canEditText) // true print("Can move locked image:", canMoveLockedImage) // false print("Can replace image:", canReplaceImage) // true print("Can move shape:", canMoveShape) // true let textEditGlobal = try engine.editor.getGlobalScope(key: "text/edit") let textEditEnabled = try engine.block.isScopeEnabled(editableText, key: "text/edit") print("Global text/edit is .defer:", textEditGlobal == .defer) // true print("Block-level text/edit enabled:", textEditEnabled) // true ``` `isAllowedByScope(_:key:)` returns the effective permission, `isScopeEnabled(_:key:)` returns only the block-level setting, and `getGlobalScope(key:)` returns only the global setting. `GlobalScope` is an Objective-C–backed enum, so compare it with `==` (for example `scope == .defer`) rather than printing it directly. ## 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 | ## Troubleshooting | Issue | Cause | Solution | | ------------------------------------ | ------------------------------------- | ---------------------------------------------------------------- | | Block still editable | Global scope set to `.allow` | Change the global scope to `.deny` or `.defer` | | Block unexpectedly locked | Global scope set to `.deny` | Set the global scope to `.defer` and enable the block-level scope | | Can't interact with unlocked block | `editor/select` scope is locked | Enable `editor/select` on blocks users should interact with | | Permission check returns wrong value | Checking the wrong scope level | Use `isAllowedByScope(_:key:)` for the effective permission | | New SDK scopes not locked | Locking code doesn't cover new scopes | Use `findAllScopes()` dynamically instead of a hardcoded list | ## Next Steps - [Rules Overview](https://img.ly/docs/cesdk/mac-catalyst/rules/overview-e27832/) — Understand the broader rules system in CE.SDK - [Lock Templates](https://img.ly/docs/cesdk/mac-catalyst/create-templates/lock-131489/) — Lock templates for consistent reuse --- ## 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: "Moderate Content" description: "Extract images and text from CE.SDK designs with the Swift Engine API, then integrate third-party moderation services to detect inappropriate content." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/rules/moderate-content-d5ff7e/" --- > 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/) > [Rules](https://img.ly/docs/cesdk/mac-catalyst/rules-1427c0/) > [Moderate Content](https://img.ly/docs/cesdk/mac-catalyst/rules/moderate-content-d5ff7e/) --- ```swift file=@cesdk_swift_examples/engine-guides-moderate-content/ModerateContent.swift reference-only import Foundation import IMGLYEngine /// Severity level derived from a moderation confidence score. private enum ModerationState { case success case warning case failed } /// A content category returned by a moderation service. private struct ModerationCategory: Sendable { let name: String let description: String let state: ModerationState } /// A graphic block with an image fill, ready to moderate. private struct ModerationImageBlock: Sendable { let blockID: DesignBlockID let blockName: String let url: URL } /// A text block, ready to moderate. private struct ModerationTextBlock: Sendable { let blockID: DesignBlockID let blockName: String let text: String } /// A moderation result tied to a specific design block. private struct ModerationResult: Sendable { let id: String let blockID: DesignBlockID let blockName: String let category: ModerationCategory let content: String } // Caches keyed by content to avoid redundant moderation calls. In production, // back these with a persistent store such as NSCache. @MainActor private var imageModerationCache: [URL: [ModerationCategory]] = [:] @MainActor private var textModerationCache: [String: [ModerationCategory]] = [:] @MainActor func moderateContent(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL // Demo scaffolding: build a page with one image block and one text block so // there is content to moderate. In your app the scene already holds the // user's content. 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) 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.setPositionX(imageBlock, value: 100) try engine.block.setPositionY(imageBlock, value: 200) try engine.block.setWidth(imageBlock, value: 500) try engine.block.setHeight(imageBlock, value: 400) try engine.block.appendChild(to: page, child: imageBlock) let textBlock = try engine.block.create(.text) try engine.block.setString(textBlock, property: "text/text", value: "Sample text content for moderation testing") try engine.block.setFloat(textBlock, property: "text/fontSize", value: 48) try engine.block.setPositionX(textBlock, value: 650) try engine.block.setPositionY(textBlock, value: 340) try engine.block.setWidth(textBlock, value: 450) try engine.block.setHeight(textBlock, value: 120) try engine.block.appendChild(to: page, child: textBlock) // Find and moderate every image and text block. let imageResults = try await checkImageContent(engine: engine) let textResults = try await checkTextContent(engine: engine) let allResults = imageResults + textResults // Report the results, highlight the first violation, and gate export on them. displayResults(allResults) try selectFirstViolation(engine: engine, results: allResults) try await exportIfAllowed(engine: engine, page: page, results: allResults) } /// Returns the image URL for a graphic block, or `nil` when the block's fill is /// not an image fill. Filtering by fill type catches every image reliably, /// regardless of the block's `kind`. @MainActor private func getImageURL(engine: Engine, blockID: DesignBlockID) -> URL? { guard let fill = try? engine.block.getFill(blockID), (try? engine.block.getType(fill)) == FillType.image.rawValue else { return nil } if let url = try? engine.block.getURL(fill, property: "fill/image/imageFileURI") { return url } if let sourceSet = try? engine.block.getSourceSet(fill, property: "fill/image/sourceSet"), let first = sourceSet.first { return first.uri } return nil } /// Finds every graphic block with an image fill and moderates each one concurrently. @MainActor private func checkImageContent(engine: Engine) async throws -> [ModerationResult] { let graphicBlockIDs = try engine.block.find(byType: .graphic) let imageBlocks: [ModerationImageBlock] = try graphicBlockIDs.compactMap { blockID in guard let url = getImageURL(engine: engine, blockID: blockID) else { return nil } return ModerationImageBlock( blockID: blockID, blockName: try engine.block.getName(blockID), url: url, ) } return try await withThrowingTaskGroup(of: [ModerationResult].self) { group in for block in imageBlocks { group.addTask { let categories = try await checkImageContentAPI(url: block.url) return categories.map { category in ModerationResult( id: "\(block.blockID)-\(category.name)", blockID: block.blockID, blockName: block.blockName, category: category, content: block.url.absoluteString, ) } } } var results: [ModerationResult] = [] for try await batch in group { results.append(contentsOf: batch) } return results } } /// Extracts the text content from a text block. @MainActor private func getTextContent(engine: Engine, blockID: DesignBlockID) -> String { (try? engine.block.getString(blockID, property: "text/text")) ?? "" } /// Finds every text block, extracts its content, and moderates each one concurrently. @MainActor private func checkTextContent(engine: Engine) async throws -> [ModerationResult] { let textBlockIDs = try engine.block.find(byType: .text) let textBlocks: [ModerationTextBlock] = try textBlockIDs.compactMap { blockID in let text = getTextContent(engine: engine, blockID: blockID) guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil } return ModerationTextBlock( blockID: blockID, blockName: try engine.block.getName(blockID), text: text, ) } return try await withThrowingTaskGroup(of: [ModerationResult].self) { group in for block in textBlocks { group.addTask { let categories = try await checkTextContentAPI(text: block.text) return categories.map { category in ModerationResult( id: "\(block.blockID)-\(category.name)", blockID: block.blockID, blockName: block.blockName, category: category, content: block.text, ) } } } var results: [ModerationResult] = [] for try await batch in group { results.append(contentsOf: batch) } return results } } /// Simulates an image moderation API call. Replace this with a real request to /// your moderation service, proxied through your backend. @MainActor private func checkImageContentAPI(url: URL) async throws -> [ModerationCategory] { if let cached = imageModerationCache[url] { return cached } // Simulate network latency before the service responds. try await Task.sleep(nanoseconds: 100_000_000) let categories = [ ModerationCategory( name: "Weapons", description: "Handguns, rifles, machine guns, threatening knives", state: percentageToState(.random(in: 0.0 ... 0.3)), ), ModerationCategory( name: "Alcohol", description: "Wine, beer, cocktails, champagne", state: percentageToState(.random(in: 0.0 ... 0.4)), ), ModerationCategory( name: "Drugs", description: "Cannabis, syringes, glass pipes, bongs, pills", state: percentageToState(.random(in: 0.0 ... 0.2)), ), ModerationCategory( name: "Nudity", description: "Raw or partial nudity", state: percentageToState(.random(in: 0.0 ... 0.3)), ), ] imageModerationCache[url] = categories return categories } /// Simulates a text moderation API call. Replace this with a real request to /// your moderation service, proxied through your backend. @MainActor private func checkTextContentAPI(text: String) async throws -> [ModerationCategory] { if let cached = textModerationCache[text] { return cached } // Simulate network latency before the service responds. try await Task.sleep(nanoseconds: 100_000_000) let categories = [ ModerationCategory( name: "Profanity", description: "Offensive or vulgar language", state: percentageToState(.random(in: 0.0 ... 0.3)), ), ModerationCategory( name: "Hate Speech", description: "Content promoting hatred or discrimination", state: percentageToState(.random(in: 0.0 ... 0.2)), ), ModerationCategory( name: "Threats", description: "Threatening or violent language", state: percentageToState(.random(in: 0.0 ... 0.1)), ), ] textModerationCache[text] = categories return categories } /// Maps a moderation confidence score to a severity level. private func percentageToState(_ percentage: Double) -> ModerationState { if percentage > 0.8 { .failed } else if percentage > 0.4 { .warning } else { .success } } /// Groups results by severity and prints a summary. private func displayResults(_ results: [ModerationResult]) { let failed = results.filter { $0.category.state == .failed } let warnings = results.filter { $0.category.state == .warning } let passed = results.filter { $0.category.state == .success } print("Content moderation results:") print("- Total checks: \(results.count)") print("- Violations: \(failed.count)") print("- Warnings: \(warnings.count)") print("- Passed: \(passed.count)") for violation in failed { print("Violation — \(violation.category.name) in \(violation.blockName): \(violation.content)") } } /// Selects the block tied to the first violation so the user can locate it. @MainActor private func selectFirstViolation(engine: Engine, results: [ModerationResult]) throws { guard let violation = results.first(where: { $0.category.state == .failed }) else { return } for selected in engine.block.findAllSelected() { try engine.block.setSelected(selected, selected: false) } try engine.block.setSelected(violation.blockID, selected: true) } /// Exports the page only when no moderation violations are present. @MainActor private func exportIfAllowed(engine: Engine, page: DesignBlockID, results: [ModerationResult]) async throws { let violations = results.filter { $0.category.state == .failed } guard violations.isEmpty else { print("Export blocked: \(violations.count) policy violation(s) detected.") return } let data = try await engine.block.export(page, mimeType: .png) print("Validation passed — exported \(data.count) bytes.") } ``` Use CE.SDK's Engine API to extract images and text from designs, then integrate third-party moderation services to detect inappropriate content. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-moderate-content) CE.SDK does not provide prebuilt content moderation workflows. Instead, it provides engine APIs that extract images and text from designs for moderation by third-party services of your choice. This is intentional: moderation requirements are specific to each business — which categories to check, what thresholds to apply, and which services to use. When and where to check content (during editing, before export, on upload) also varies with your workflow. Content moderation helps maintain quality standards and comply with content policies. Unlike built-in validation rules that check technical aspects like resolution or layout, content moderation relies on external AI services (Sightengine, AWS Rekognition, OpenAI Moderation API) to analyze visual content (images for weapons, drugs, nudity) and textual content (profanity, hate speech, threats). Each finding is a `ModerationResult` built from a `ModerationCategory`. The example source also defines the small `ModerationImageBlock` and `ModerationTextBlock` value types that the extraction helpers return. ## Finding Content in Designs Locate all images and text blocks, then extract the data needed for moderation. **Images**: Use `find(byType:)` with `DesignBlockType.graphic` to find graphic blocks, then keep the ones whose fill is an image fill. Comparing the fill's type to `FillType.image` reliably catches every image — a block's `kind` is a free-form string that isn't guaranteed to be set, so filtering by fill type is the dependable approach. `getFill(_:)` returns the fill block, and `getURL(_:property:)` reads its `fill/image/imageFileURI` property as a Swift `URL`, with the source set as a fallback: ```swift highlight-moderateContent-getImageURL /// Returns the image URL for a graphic block, or `nil` when the block's fill is /// not an image fill. Filtering by fill type catches every image reliably, /// regardless of the block's `kind`. @MainActor private func getImageURL(engine: Engine, blockID: DesignBlockID) -> URL? { guard let fill = try? engine.block.getFill(blockID), (try? engine.block.getType(fill)) == FillType.image.rawValue else { return nil } if let url = try? engine.block.getURL(fill, property: "fill/image/imageFileURI") { return url } if let sourceSet = try? engine.block.getSourceSet(fill, property: "fill/image/sourceSet"), let first = sourceSet.first { return first.uri } return nil } ``` Process every image by checking each URL against the moderation service: ```swift highlight-moderateContent-checkAllImages /// Finds every graphic block with an image fill and moderates each one concurrently. @MainActor private func checkImageContent(engine: Engine) async throws -> [ModerationResult] { let graphicBlockIDs = try engine.block.find(byType: .graphic) let imageBlocks: [ModerationImageBlock] = try graphicBlockIDs.compactMap { blockID in guard let url = getImageURL(engine: engine, blockID: blockID) else { return nil } return ModerationImageBlock( blockID: blockID, blockName: try engine.block.getName(blockID), url: url, ) } return try await withThrowingTaskGroup(of: [ModerationResult].self) { group in for block in imageBlocks { group.addTask { let categories = try await checkImageContentAPI(url: block.url) return categories.map { category in ModerationResult( id: "\(block.blockID)-\(category.name)", blockID: block.blockID, blockName: block.blockName, category: category, content: block.url.absoluteString, ) } } } var results: [ModerationResult] = [] for try await batch in group { results.append(contentsOf: batch) } return results } } ``` **Text**: Use `find(byType:)` with `DesignBlockType.text` to find text blocks, then read their content from the `text/text` property: ```swift highlight-moderateContent-getTextContent /// Extracts the text content from a text block. @MainActor private func getTextContent(engine: Engine, blockID: DesignBlockID) -> String { (try? engine.block.getString(blockID, property: "text/text")) ?? "" } ``` Process every text block by checking each string against the moderation service: ```swift highlight-moderateContent-checkAllText /// Finds every text block, extracts its content, and moderates each one concurrently. @MainActor private func checkTextContent(engine: Engine) async throws -> [ModerationResult] { let textBlockIDs = try engine.block.find(byType: .text) let textBlocks: [ModerationTextBlock] = try textBlockIDs.compactMap { blockID in let text = getTextContent(engine: engine, blockID: blockID) guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil } return ModerationTextBlock( blockID: blockID, blockName: try engine.block.getName(blockID), text: text, ) } return try await withThrowingTaskGroup(of: [ModerationResult].self) { group in for block in textBlocks { group.addTask { let categories = try await checkTextContentAPI(text: block.text) return categories.map { category in ModerationResult( id: "\(block.blockID)-\(category.name)", blockID: block.blockID, blockName: block.blockName, category: category, content: block.text, ) } } } var results: [ModerationResult] = [] for try await batch in group { results.append(contentsOf: batch) } return results } } ``` Both checks use a `withThrowingTaskGroup` to moderate multiple items concurrently. ## Integrating Moderation APIs Integrate external AI services (Sightengine, AWS Rekognition, OpenAI Moderation API) to analyze content. Always proxy requests through your backend so credentials never ship in your app, and apply rate limiting and authentication there. **Image Moderation** — The example uses a simulated API call that you replace with your moderation service. It returns content categories with confidence scores and caches them by URL to avoid redundant calls: ```swift highlight-moderateContent-imageModerationAPI /// Simulates an image moderation API call. Replace this with a real request to /// your moderation service, proxied through your backend. @MainActor private func checkImageContentAPI(url: URL) async throws -> [ModerationCategory] { if let cached = imageModerationCache[url] { return cached } // Simulate network latency before the service responds. try await Task.sleep(nanoseconds: 100_000_000) let categories = [ ModerationCategory( name: "Weapons", description: "Handguns, rifles, machine guns, threatening knives", state: percentageToState(.random(in: 0.0 ... 0.3)), ), ModerationCategory( name: "Alcohol", description: "Wine, beer, cocktails, champagne", state: percentageToState(.random(in: 0.0 ... 0.4)), ), ModerationCategory( name: "Drugs", description: "Cannabis, syringes, glass pipes, bongs, pills", state: percentageToState(.random(in: 0.0 ... 0.2)), ), ModerationCategory( name: "Nudity", description: "Raw or partial nudity", state: percentageToState(.random(in: 0.0 ... 0.3)), ), ] imageModerationCache[url] = categories return categories } ``` In production, replace the simulated body with a request to your backend endpoint that proxies services like Sightengine or AWS Rekognition. **Text Moderation** — The text check follows the same shape. Replace the simulated body with your text moderation service: ```swift highlight-moderateContent-textModerationAPI /// Simulates a text moderation API call. Replace this with a real request to /// your moderation service, proxied through your backend. @MainActor private func checkTextContentAPI(text: String) async throws -> [ModerationCategory] { if let cached = textModerationCache[text] { return cached } // Simulate network latency before the service responds. try await Task.sleep(nanoseconds: 100_000_000) let categories = [ ModerationCategory( name: "Profanity", description: "Offensive or vulgar language", state: percentageToState(.random(in: 0.0 ... 0.3)), ), ModerationCategory( name: "Hate Speech", description: "Content promoting hatred or discrimination", state: percentageToState(.random(in: 0.0 ... 0.2)), ), ModerationCategory( name: "Threats", description: "Threatening or violent language", state: percentageToState(.random(in: 0.0 ... 0.1)), ), ] textModerationCache[text] = categories return categories } ``` In production, replace the simulation with calls to services like the OpenAI Moderation API or Perspective API through your backend. **Processing Results** — Map confidence scores to severity levels (failed above `0.8`, warning above `0.4`, success at or below `0.4`): ```swift highlight-moderateContent-thresholdMapping /// Maps a moderation confidence score to a severity level. private func percentageToState(_ percentage: Double) -> ModerationState { if percentage > 0.8 { .failed } else if percentage > 0.4 { .warning } else { .success } } ``` ## Displaying Validation Results Group results by severity and surface them to the user: ```swift highlight-moderateContent-displayResults /// Groups results by severity and prints a summary. private func displayResults(_ results: [ModerationResult]) { let failed = results.filter { $0.category.state == .failed } let warnings = results.filter { $0.category.state == .warning } let passed = results.filter { $0.category.state == .success } print("Content moderation results:") print("- Total checks: \(results.count)") print("- Violations: \(failed.count)") print("- Warnings: \(warnings.count)") print("- Passed: \(passed.count)") for violation in failed { print("Violation — \(violation.category.name) in \(violation.blockName): \(violation.content)") } } ``` Make results actionable by selecting the corresponding block when the user picks a finding. This helps them locate problematic content on the canvas. `findAllSelected()` returns the current selection so you can clear it before selecting the flagged block: ```swift highlight-moderateContent-interactiveResults /// Selects the block tied to the first violation so the user can locate it. @MainActor private func selectFirstViolation(engine: Engine, results: [ModerationResult]) throws { guard let violation = results.first(where: { $0.category.state == .failed }) else { return } for selected in engine.block.findAllSelected() { try engine.block.setSelected(selected, selected: false) } try engine.block.setSelected(violation.blockID, selected: true) } ``` ## Integration Points Run validation at the point that fits your workflow. The most common gate is export: validate the design and refuse to render it when violations exist. The example checks the results and only then renders the page to PNG with `engine.block.export(_:mimeType:)` using `MIMEType.png`: ```swift highlight-moderateContent-exportValidation /// Exports the page only when no moderation violations are present. @MainActor private func exportIfAllowed(engine: Engine, page: DesignBlockID, results: [ModerationResult]) async throws { let violations = results.filter { $0.category.state == .failed } guard violations.isEmpty else { print("Export blocked: \(violations.count) policy violation(s) detected.") return } let data = try await engine.block.export(page, mimeType: .png) print("Validation passed — exported \(data.count) bytes.") } ``` Other integration points follow the same pattern: - **Pre-upload validation**: Check before allowing uploads to your platform. - **Review queue**: Flag designs with warnings for manual review. - **Batch validation**: Check all content on demand when the user requests it. ## Best Practices **Security**: Always proxy moderation requests through your backend to protect credentials. Apply rate limiting and require authentication. Log checks for compliance and auditing. **Performance**: Cache results by URL and text to avoid redundant calls. Moderate multiple items concurrently with a task group. **User Experience**: Run checks asynchronously so they never block the main actor. Provide clear, actionable messages and let users jump to flagged blocks. **Timing**: Validate at export time for the best balance between policy enforcement and creative freedom. ## Troubleshooting **Checks not running**: Verify the engine is initialized, content exists, the moderation endpoint is reachable, and credentials are valid. **Content not found**: Ensure graphic blocks have an image fill, text blocks aren't empty, and the scene has loaded. Find image blocks with `find(byType: .graphic)` and keep those whose fill type is `FillType.image`. Filtering by a block's `kind` is unreliable — the kind is a free-form string that may be unset on blocks created or loaded outside the editor's image flow. **API errors**: Check API key validity, endpoint URL, image URL accessibility, rate limits, and service-specific error codes. **Inconsistent results**: Verify caching behaves as expected, threshold values are appropriate, and API responses parse correctly. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.find(byType:)` | Find all blocks of a `DesignBlockType`, for example `.graphic` or `.text` | | `engine.block.getFill(_:)` | Get the fill block for a graphic block | | `engine.block.getType(_:)` | Read a block's or fill's type identifier; compare a fill's type to `FillType.image.rawValue` to detect image fills | | `engine.block.getURL(_:property:)` | Read a `URL` property such as `fill/image/imageFileURI` | | `engine.block.getString(_:property:)` | Read a string property such as `text/text` | | `engine.block.getSourceSet(_:property:)` | Read the image source set as `[Source]` (each `Source.uri` is a `URL`) | | `engine.block.getName(_:)` | Read a block's display name | | `engine.block.setSelected(_:selected:)` | Select or deselect a block | | `engine.block.findAllSelected()` | Get the currently selected blocks | | `engine.block.export(_:mimeType:)` | Render a block to image data | ### Properties | Property | Type | Description | | --- | --- | --- | | `fill/image/imageFileURI` | String | Primary image URL | | `fill/image/sourceSet` | Array | Responsive image sources with URIs | | `text/text` | String | Text content of a text block | ## Next Steps Now that you understand content moderation, explore related validation features: - [Rules Overview](https://img.ly/docs/cesdk/mac-catalyst/rules/overview-e27832/) — Understand the scopes and permission model behind CE.SDK 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: "Overview" description: "Define and enforce layout, branding, and safety rules to ensure consistent and compliant designs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/rules/overview-e27832/" --- > 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/) > [Rules](https://img.ly/docs/cesdk/mac-catalyst/rules-1427c0/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/rules/overview-e27832/) --- ```swift file=@cesdk_swift_examples/engine-guides-rules-overview/RulesOverview.swift reference-only import IMGLYEngine @MainActor func rulesOverview(engine: Engine) async throws { // Set up a design scene with a page to host the demo blocks. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 1600) try engine.block.setHeight(page, value: 1000) try engine.block.appendChild(to: scene, child: page) // The default Creator role allows every scope globally, which would short-circuit // the block-level checks below — set each scope to `.defer` to honor per-block settings. // Layer operations 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.editor.setGlobalScope(key: "layer/flip", value: .defer) try engine.editor.setGlobalScope(key: "layer/crop", value: .defer) try engine.editor.setGlobalScope(key: "layer/opacity", value: .defer) try engine.editor.setGlobalScope(key: "layer/blendMode", value: .defer) try engine.editor.setGlobalScope(key: "layer/visibility", value: .defer) try engine.editor.setGlobalScope(key: "layer/clipping", value: .defer) // Appearance try engine.editor.setGlobalScope(key: "appearance/adjustments", value: .defer) try engine.editor.setGlobalScope(key: "appearance/filter", value: .defer) try engine.editor.setGlobalScope(key: "appearance/effect", value: .defer) try engine.editor.setGlobalScope(key: "appearance/blur", value: .defer) try engine.editor.setGlobalScope(key: "appearance/shadow", value: .defer) // Content editing try engine.editor.setGlobalScope(key: "fill/change", value: .defer) try engine.editor.setGlobalScope(key: "fill/changeType", value: .defer) try engine.editor.setGlobalScope(key: "stroke/change", value: .defer) // Lifecycle try engine.editor.setGlobalScope(key: "lifecycle/destroy", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/duplicate", value: .defer) try engine.editor.setGlobalScope(key: "editor/add", value: .defer) try engine.editor.setGlobalScope(key: "editor/select", value: .defer) // Create five demo blocks, one per scope configuration. Each is a gray // rectangle; only the name and per-block scope settings differ. let blockNames = [ "Layer Operations Disabled", "Appearance Disabled", "Content Editing Disabled", "All Scopes Disabled", "All Scopes Enabled", ] var demoBlocks: [DesignBlockID] = [] for name in blockNames { let block = try engine.block.create(.graphic) try engine.block.setShape(block, shape: engine.block.createShape(.rect)) try engine.block.setWidth(block, value: 300) try engine.block.setHeight(block, value: 300) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: .rgba(r: 0.6, g: 0.6, b: 0.6, a: 1)) try engine.block.setFill(block, fill: fill) try engine.block.appendChild(to: page, child: block) try engine.block.setName(block, name: name) demoBlocks.append(block) } let layerBlock = demoBlocks[0] let appearanceBlock = demoBlocks[1] let contentBlock = demoBlocks[2] let lockedBlock = demoBlocks[3] let enabledBlock = demoBlocks[4] // The complete set of scopes, used to fully lock or fully unlock a block. let allScopes = [ "layer/move", "layer/resize", "layer/rotate", "layer/flip", "layer/crop", "layer/opacity", "layer/blendMode", "layer/visibility", "layer/clipping", "appearance/adjustments", "appearance/filter", "appearance/effect", "appearance/blur", "appearance/shadow", "fill/change", "fill/changeType", "stroke/change", "lifecycle/destroy", "lifecycle/duplicate", "editor/add", "editor/select", ] try engine.block.setScopeEnabled(layerBlock, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/resize", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/rotate", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/flip", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/crop", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/opacity", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/blendMode", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/visibility", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/clipping", enabled: false) // Keep other categories editable. try engine.block.setScopeEnabled(layerBlock, key: "fill/change", enabled: true) try engine.block.setScopeEnabled(layerBlock, key: "lifecycle/destroy", enabled: true) try engine.block.setScopeEnabled(layerBlock, key: "editor/select", enabled: true) // Block 2: disable the appearance scopes. try engine.block.setScopeEnabled(appearanceBlock, key: "appearance/adjustments", enabled: false) try engine.block.setScopeEnabled(appearanceBlock, key: "appearance/filter", enabled: false) try engine.block.setScopeEnabled(appearanceBlock, key: "appearance/effect", enabled: false) try engine.block.setScopeEnabled(appearanceBlock, key: "appearance/blur", enabled: false) try engine.block.setScopeEnabled(appearanceBlock, key: "appearance/shadow", enabled: false) // Block 3: disable the content-editing scopes. try engine.block.setScopeEnabled(contentBlock, key: "fill/change", enabled: false) try engine.block.setScopeEnabled(contentBlock, key: "fill/changeType", enabled: false) try engine.block.setScopeEnabled(contentBlock, key: "stroke/change", enabled: false) // Block 4: disable every scope, fully locking the block. for scope in allScopes { try engine.block.setScopeEnabled(lockedBlock, key: scope, enabled: false) } // Block 5: enable every scope, leaving the block fully editable. for scope in allScopes { try engine.block.setScopeEnabled(enabledBlock, key: scope, enabled: true) } let canMoveLayer = try engine.block.isAllowedByScope(layerBlock, key: "layer/move") let canMoveEnabled = try engine.block.isAllowedByScope(enabledBlock, key: "layer/move") let canMoveLocked = try engine.block.isAllowedByScope(lockedBlock, key: "layer/move") print("Layer block - can move: \(canMoveLayer)") // false print("Enabled block - can move: \(canMoveEnabled)") // true print("Locked block - can move: \(canMoveLocked)") // false try engine.editor.setGlobalScope(key: "layer/flip", value: .deny) let canFlipEnabled = try engine.block.isAllowedByScope(enabledBlock, key: "layer/flip") print("Enabled block - can flip after global deny: \(canFlipEnabled)") // false } ``` Learn how CE.SDK's rules system enforces design constraints and controls editing permissions through the scopes mechanism. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-rules-overview) In CE.SDK, *rules* are the design constraints and guardrails that control which editing operations are permitted. The primary mechanism for enforcing rules is the **scopes system** — permission flags that let you build guided editing experiences, maintain brand consistency, ensure design quality, and prevent unauthorized modifications. This guide covers the scopes system conceptually: the available scope categories, the difference between global and block-level scopes, and how to resolve the effective permission for any block. For detailed implementation of specific rule use cases, see the guides linked under Common Use Cases. ## What Are Scopes Scopes are permission flags that control specific editing capabilities. Each scope maps to a particular operation category, identified by a string key such as `layer/move` or `fill/change`. CE.SDK organizes scopes into four categories: **Layer operations** — Control positioning and transformation: - `layer/move`, `layer/resize`, `layer/rotate`, `layer/flip` - `layer/crop`, `layer/opacity`, `layer/blendMode` - `layer/visibility`, `layer/clipping` **Appearance** — Control visual effects and adjustments: - `appearance/adjustments`, `appearance/filter`, `appearance/effect` - `appearance/blur`, `appearance/shadow`, `appearance/animation` **Content editing** — Control content modifications: - `text/edit`, `text/character` - `fill/change`, `fill/changeType` - `stroke/change`, `shape/change` **Lifecycle** — Control block management: - `lifecycle/destroy`, `lifecycle/duplicate` - `editor/add`, `editor/select` ## Setting Global Scopes Global scopes set editor-wide defaults that apply to every block. Use `setGlobalScope(key:value:)` with one of three `GlobalScope` permission levels: - `.allow` — The operation is always permitted for all blocks. - `.deny` — The operation is always blocked for all blocks. - `.defer` — Control is deferred to each block's individual scope setting. ```swift highlight-rulesOverview-globalScope // The default Creator role allows every scope globally, which would short-circuit // the block-level checks below — set each scope to `.defer` to honor per-block settings. // Layer operations 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.editor.setGlobalScope(key: "layer/flip", value: .defer) try engine.editor.setGlobalScope(key: "layer/crop", value: .defer) try engine.editor.setGlobalScope(key: "layer/opacity", value: .defer) try engine.editor.setGlobalScope(key: "layer/blendMode", value: .defer) try engine.editor.setGlobalScope(key: "layer/visibility", value: .defer) try engine.editor.setGlobalScope(key: "layer/clipping", value: .defer) // Appearance try engine.editor.setGlobalScope(key: "appearance/adjustments", value: .defer) try engine.editor.setGlobalScope(key: "appearance/filter", value: .defer) try engine.editor.setGlobalScope(key: "appearance/effect", value: .defer) try engine.editor.setGlobalScope(key: "appearance/blur", value: .defer) try engine.editor.setGlobalScope(key: "appearance/shadow", value: .defer) // Content editing try engine.editor.setGlobalScope(key: "fill/change", value: .defer) try engine.editor.setGlobalScope(key: "fill/changeType", value: .defer) try engine.editor.setGlobalScope(key: "stroke/change", value: .defer) // Lifecycle try engine.editor.setGlobalScope(key: "lifecycle/destroy", value: .defer) try engine.editor.setGlobalScope(key: "lifecycle/duplicate", value: .defer) try engine.editor.setGlobalScope(key: "editor/add", value: .defer) try engine.editor.setGlobalScope(key: "editor/select", value: .defer) ``` When a scope is set to `.defer`, the effective permission comes from the block-level setting, enabling fine-grained per-element control. Read the current value back with `getGlobalScope(key:)`. ## Setting Block-Level Scopes Block-level scopes override a deferred global setting for individual blocks. Use `setScopeEnabled(_:key:enabled:)` to enable or disable an operation on a specific block. The example creates five demo blocks and configures each with a different scope category: - **`layerBlock`** — Layer operations disabled; other categories editable. - **`appearanceBlock`** — Appearance scopes disabled. - **`contentBlock`** — Content-editing scopes disabled. - **`lockedBlock`** — Every scope disabled, fully locking the block. - **`enabledBlock`** — Every scope enabled, leaving the block fully editable. The first block disables all layer operations while keeping the remaining categories editable: ```swift highlight-rulesOverview-blockScope try engine.block.setScopeEnabled(layerBlock, key: "layer/move", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/resize", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/rotate", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/flip", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/crop", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/opacity", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/blendMode", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/visibility", enabled: false) try engine.block.setScopeEnabled(layerBlock, key: "layer/clipping", enabled: false) // Keep other categories editable. try engine.block.setScopeEnabled(layerBlock, key: "fill/change", enabled: true) try engine.block.setScopeEnabled(layerBlock, key: "lifecycle/destroy", enabled: true) try engine.block.setScopeEnabled(layerBlock, key: "editor/select", enabled: true) ``` Block-level settings only take effect when the matching global scope is `.defer`. Query a block's current setting with `isScopeEnabled(_:key:)`. ## Checking Scope Permissions Before performing an operation programmatically, verify that it is allowed with `isAllowedByScope(_:key:)`. This method resolves the effective permission from both the global and block-level settings: ```swift highlight-rulesOverview-checkScope let canMoveLayer = try engine.block.isAllowedByScope(layerBlock, key: "layer/move") let canMoveEnabled = try engine.block.isAllowedByScope(enabledBlock, key: "layer/move") let canMoveLocked = try engine.block.isAllowedByScope(lockedBlock, key: "layer/move") print("Layer block - can move: \(canMoveLayer)") // false print("Enabled block - can move: \(canMoveEnabled)") // true print("Locked block - can move: \(canMoveLocked)") // false ``` A global `.deny` blocks an operation on every block regardless of its block-level setting — even a fully enabled block can no longer perform the operation: ```swift highlight-rulesOverview-denyGlobal try engine.editor.setGlobalScope(key: "layer/flip", value: .deny) let canFlipEnabled = try engine.block.isAllowedByScope(enabledBlock, key: "layer/flip") print("Enabled block - can flip after global deny: \(canFlipEnabled)") // false ``` ## Common Use Cases The scopes system supports a range of rule-enforcement scenarios: - [Lock Content](https://img.ly/docs/cesdk/mac-catalyst/rules/lock-content-9fa727/) — Prevent modifications to specific elements such as logos or legal text. - **Define safe zones** — Mark areas where content must remain for correct trimming. - [Enforce Brand Guidelines](https://img.ly/docs/cesdk/mac-catalyst/rules/enforce-brand-guidelines-23a1e3/) — Restrict fonts, colors, and styles to approved options. - [Moderate Content](https://img.ly/docs/cesdk/mac-catalyst/rules/moderate-content-d5ff7e/) — Integrate external services to validate content appropriateness. ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `engine.editor.setGlobalScope(key:value:)` | Global | Set an editor-wide scope permission | | `engine.editor.getGlobalScope(key:)` | Global | Get the current global scope value | | `engine.block.setScopeEnabled(_:key:enabled:)` | Block | Enable or disable a scope for a specific block | | `engine.block.isScopeEnabled(_:key:)` | Block | Check whether a scope is enabled for a block | | `engine.block.isAllowedByScope(_:key:)` | Block | Check whether an operation is allowed | --- ## 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: "Security" description: "Learn how CE.SDK keeps your data private with client-side processing, secure licensing, and GDPR-compliant practices." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/security-777bfd/" --- > 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/) > [Security](https://img.ly/docs/cesdk/mac-catalyst/security-777bfd/) --- This document provides a comprehensive overview of CE.SDK's security practices, focusing on data handling, privacy, and our commitment to maintaining the highest standards of security for our customers and their end users. ## Key Security Features - **Client-Side Processing**: All image and design processing occurs directly on the user's device or your servers, not on our servers - **No Data Transmission**: Your content (e.g. images, designs, templates, videos, audio, etc.) is never uploaded to or processed on IMG.LY servers - **Minimal Data Collection**: We only collect device identifiers and count exports for licensing purposes - **GDPR Compliance**: Our data collection practices adhere to GDPR regulations - **Secure Licensing**: Enterprise licenses are secured with RSA SHA256 encryption ## Data Protection & Access Controls ### Data Collection CE.SDK requires minimal data to provide its services. The only potentially personally identifiable information (PII) collected includes device-specific identifiers such as `identifierForVendor` on iOS and `ANDROID_ID` on Android. These identifiers are: - Used solely for tracking monthly active users for our usage-based pricing models - Reset when the user reinstalls the app or resets their device - Collected under GDPR's legitimate interest provision (no explicit consent required as they are necessary for our licensing system) Additionally, we track export operations for billing purposes in usage-based pricing models. For enterprise customers who prefer more accurate tracking, integrators can provide their own userID. This allows for more precise measurement of usage without requiring additional device identifiers. ### Data Storage & Encryption **We do not collect or store user data beyond the device identifiers and export counts mentioned above.** Since CE.SDK operates entirely client-side: - All content processing happens on the user's device - No images, designs, or user content is transmitted to IMG.LY servers - No content data is stored on IMG.LY infrastructure We use standard HTTPS (SSL/TLS) encryption for all communications between CE.SDK instances and our licensing backend. ### Access Controls We are using established industry standard practices to handle sensitive customer data. Therefore access control concerns are minimized. The limited data we do handle is protected as follows: - Billing information is stored in Stripe and accessed only by members of our finance team and C-level executives - API keys and credentials are stored securely in 1Password or GitHub with granular access levels - All employees sign Confidentiality Agreements to protect customer information This refers to data of our direct customers, not their users or customers. ## Licensing System CE.SDK uses a licensing system that works as follows: 1. During instantiation, an API key is provided to the CE.SDK instance 2. This API key is held in memory (never stored permanently on the device) 3. The SDK validates the key with our licensing backend 4. Upon successful validation, the backend returns a temporary local license 5. This license is periodically refreshed to maintain valid usage For browser implementations, we protect licenses against misuse by pinning them to specific domains. For mobile applications, licenses are pinned to the application identifiers to prevent unauthorized use. For enterprise customers, we offer an alternative model: - A license file is passed directly to the instance - No communication with our licensing service is required - Licenses are secured using RSA SHA256 encryption ### CE.SDK Renderer CE.SDK Renderer is a specialized variant of CE.SDK that consists of a native Linux binary bundled in a Docker container. It uses GPU acceleration and native code to render scenes and archives to various export formats. Due to bundled third-party codecs (mainly H.264 & H.265) and their associated patent requirements, CE.SDK Renderer implements additional licensing communication beyond the standard licensing handshake: 1. **Initial License Validation**: The tool performs the standard license validation with our licensing backend 2. **Periodic Heartbeats**: After successful validation, it sends periodic heartbeats to our licensing backend to track the number of active instances 3. **Instance Limits**: We limit the maximum number of active instances per license based on the settings in your dashboard 4. **Activation Control**: If the instance limit is exceeded, further activations (launches) of the tool will fail with a descriptive error message This additional communication allows us to ensure compliance with codec licensing requirements while providing transparent usage tracking for your organization. As with all CE.SDK products, no user data (images, videos, designs, or other content) is transmitted to IMG.LY servers - only device identifiers and instance counts are collected for licensing purposes. ## Security Considerations for User Input As CE.SDK deals primarily with arbitrary user input, we've implemented specific security measures to handle data safely: - The CreativeEngine reads files from external resources to fetch images, fonts, structured data, and other sources for designs. These reads are safeguarded by platform-specific default measures. - The engine never loads executable code or attempts to execute any data acquired from dynamic content. It generally relies on provided mime types to decode image data or falls back to byte-level inspection to choose the appropriate decoder. - For data writing operations, we provide a callback that returns a pointer to the to-be-written data. The engine itself never unconditionally writes to an externally defined path. If it writes to files directly, these are part of internal directories and can't be modified externally. - Generated PDFs may have original image files embedded if the image was not altered via effects or blurs and the `exportPdfWithHighCompatibility` option was **not** enabled. This means a malicious image file could theoretically be included in the exported PDF. - Inline text-editing allows arbitrary input of strings by users. The engine uses platform-specific default inputs and APIs and doesn't apply additional sanitization. The acquired strings are stored and used exclusively for text rendering - they are neither executed nor used for file operations. ## Security Infrastructure ### Vulnerability Management We take a proactive approach to security vulnerability management: - We use GitHub to track dependency vulnerabilities - We regularly update affected dependencies - We don't maintain a private network, eliminating network vulnerability concerns in that context - We don't manually maintain servers or infrastructure, as we don't have live systems beyond those required for licensing - For storage and licensing, we use virtual instances in Google Cloud which are managed by the cloud provider - All security-related fixes are published in our public changelog at [https://img.ly/docs/cesdk/changelog/](https://img.ly/docs/cesdk/changelog/) ### Security Development Practices Our development practices emphasize security: - We rely on established libraries with proven security track records - We don't directly process sensitive user data in our code - Secrets (auth tokens, passwords, API credentials, certificates) are stored in GitHub or 1Password with granular access levels - We use RSA SHA256 encryption for our enterprise licenses - We rely on platform-standard SSL implementations for HTTPS communications ### API Key Management API keys for CE.SDK are handled securely: - Keys are passed during instantiation and held in memory only - Keys are never stored permanently on client devices - For web implementation, keys are pinned to specific domains to prevent unauthorized use - Enterprise licenses use a file-based approach that doesn't require API key validation ## Compliance IMG.LY complies with the General Data Protection Regulation (GDPR) in all our operations, including CE.SDK. Our Privacy Policy is publicly available at [https://img.ly/privacy-policy](https://img.ly/privacy-policy). Our client-side approach to content processing significantly reduces privacy and compliance concerns, as user content never leaves their device environment for processing. ## FAQ ### Does CE.SDK upload my images or designs to IMG.LY servers? No. CE.SDK processes all content locally on the user's device. Your images, designs, and other content are never transmitted to IMG.LY servers. ### What data does IMG.LY collect through CE.SDK? CE.SDK only collects device identifiers (such as identifierForVendor on iOS or ANDROID\_ID on Android) for licensing purposes and export counts. No user content or personal information is collected. ### How does IMG.LY protect API keys? API keys are never stored permanently; they are held in memory during SDK operation. For web implementations, keys are pinned to specific domains to prevent unauthorized use. ### Has IMG.LY experienced any security breaches? No, IMG.LY has not been involved in any cybersecurity breaches in the last 12 months. ### Does IMG.LY conduct security audits? As we don't store customer data directly, but rely on third parties to do so, we focus our security efforts on dependency tracking and vulnerability management through GitHub's security features. We don't conduct security audits. ## Additional Information For more detailed information about our data collection practices, please refer to our Data Privacy and Retention information below. Should you have any additional questions regarding security practices or require more information, please contact our team at [support@img.ly](mailto:support@img.ly). ## Data Privacy and Retention At IMG.LY, we prioritize your data privacy and ensure that apart from a minimal contractually stipulated set of interactions with our servers all other operations take place on your local device. Below is an overview of our data privacy and retention policies: ### **Data Processing** All data processed by CE.SDK remains strictly on your device. We do not transfer your data to our servers for processing. This means that operations such as rendering, editing, and other in-app functionalities happen entirely locally, ensuring that sensitive project or personal data stays with you. ### **Data Retention** We do not store any project-related data on our servers. Since all data operations occur locally, no information about your edits, images, or video content is retained by CE.SDK. The only data that interacts with our servers is related to license validation and telemetry related to usage tied to your pricing plan. ### **License Validation** CE.SDK performs a license validation check with our servers once upon initialization to validate the software license being used. This interaction is minimal and does not involve the transfer of any personal, project, or media data. ### **Event Tracking** While CE.SDK does not track user actions other than the exceptions listed below through telemetry or analytics by default, there are specific events tracked to manage customer usage, particularly for API key usage tracking. We gather the following information during these events: - **When the engine loads:** App identifier, platform, engine version, user ID (provided by the client), device ID (mobile only), and session ID. - **When a photo or video is exported:** User ID, device ID, session ID, media type (photo/video), format, resolution (width and height), page count (photo only), FPS (video only), and duration (video only). This tracking is solely for ensuring accurate usage calculation and managing monthly active user billing. Enterprise clients can opt out of this tracking under specific agreements. For a precise definition of which operations count as an export and when export events are recorded, see [Export Counting](https://img.ly/docs/cesdk/mac-catalyst/export-counting-613923/). ### **Personal Identifiable Information (PII)** The only PII that is potentially collected includes device-specific identifiers such as `identifierForVendor` on iOS and `ANDROID_ID` on Android. These IDs are used for tracking purposes and are reset when the user reinstalls the app or resets the device. No consent is required for these identifiers because they are crucial for our usage-based pricing models. This is covered by the GDPR as legitimate interest. ### **User Consent** As mentioned above, user consent is not required when solely using the CE.SDK. However, this may change depending on the specific enterprise agreement or additional regulatory requirements. IMG.LY is committed to maintaining compliance with **GDPR** and other applicable data protection laws, ensuring your privacy is respected at all times. For details consult our [privacy policy](https://img.ly/privacy-policy). --- ## 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: "Serve Assets From Your Server" description: "Set up and manage how assets are served to the editor, including local, remote, or CDN-based delivery." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/" --- > 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/) > [Serve Assets](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) --- ```swift file=@cesdk_swift_examples/engine-guides-serve-assets/ServeAssets.swift reference-only import Foundation import IMGLYEngine private let serveAssetsDefaultSourceIDs = [ "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.styles", "ly.img.text.curves", "ly.img.text.components", ] private let serveAssetsDemoSourceIDs = [ "ly.img.image", "ly.img.video", "ly.img.audio", "ly.img.templates", "ly.img.templates.premium", ] @MainActor func serveAssets(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL for id in serveAssetsDefaultSourceIDs { _ = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent(id).appendingPathComponent("content.json"), ) } try engine.editor.setSettingString("basePath", value: baseURL.absoluteString) } // Register the sample content sources (images, videos, audio, templates). These // ship in the same archive and load the same way — replace them with your own // content sources in production. @MainActor func serveAssetsSampleContent(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL for id in serveAssetsDemoSourceIDs { _ = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent(id).appendingPathComponent("content.json"), ) } } // Variations showing where to host the assets. These are compile-only // demonstrations — the test runs `serveAssets` against the bundled assets. @MainActor func serveAssetsFromRemoteServer(engine: Engine) async throws { let baseURL = URL(string: "https://cdn.your.custom.domain/assets")! for id in serveAssetsDefaultSourceIDs { _ = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent(id).appendingPathComponent("content.json"), ) } } @MainActor func serveAssetsFromAppBundle(engine: Engine) async throws { guard let baseURL = Bundle.main.url(forResource: "IMGLYAssets", withExtension: "bundle") else { return } for id in serveAssetsDefaultSourceIDs { _ = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent(id).appendingPathComponent("content.json"), ) } } ``` Configure the Creative Engine to load its asset sources from your own server or app bundle instead of the IMG.LY CDN. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-serve-assets) The engine serves all assets from the IMG.LY CDN by default, which is convenient while you are getting started. For production you should serve them from your own location so your app doesn't depend on the IMG.LY CDN at runtime — it gives you control over performance and availability and keeps you within your own compliance boundary. ## Download the Assets The assets are versioned alongside the SDK, so always download the archive that matches your engine version — content from a different version may not be compatible. Asset versions are platform-specific: the iOS and Android archives are usually aligned, but the web SDK can move at a different pace, so download from the `cesdk-swift` path that matches your engine version. [Download Assets (v$UBQ\_VERSION$)](https://cdn.img.ly/packages/imgly/cesdk-swift/$UBQ_VERSION$/imgly-assets.zip) Or download and extract it from the command line: ```bash curl -O https://cdn.img.ly/packages/imgly/cesdk-swift/$UBQ_VERSION$/imgly-assets.zip unzip imgly-assets.zip -d IMGLYAssets.bundle ``` The archive contains one `ly.img.*` directory per asset source, the `fonts/` and `emoji/` directories, and thumbnail directories such as `ly.img.animation` that serve preview images for the engine's built-in animation presets. Those thumbnail directories have no `content.json` and aren't registered as asset sources — keep them at your `baseURL` so the previews resolve. To ship the assets inside your app, add the extracted `.bundle` folder to your app target — this produces a nested `Bundle` your app can resolve at runtime. Alternatively, upload the extracted folders to your own server or CDN to serve them remotely. ## Register the Default Asset Sources Each asset source is described by a `content.json` manifest. Register a source by pointing `engine.asset.addLocalAssetSourceFromJSON(_:)` at its manifest URL; the engine resolves the asset files relative to that URL. Loop over the default source IDs and load each manifest from a single `baseURL` that points at your asset location: ```swift highlight-serveAssets-defaultSourceIDs private let serveAssetsDefaultSourceIDs = [ "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.styles", "ly.img.text.curves", "ly.img.text.components", ] ``` ```swift highlight-serveAssets-registerDefaults for id in serveAssetsDefaultSourceIDs { _ = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent(id).appendingPathComponent("content.json"), ) } ``` `baseURL` points at your hosting location — set it to your own server or app bundle as shown below. The default sources are: - `ly.img.sticker` — Stickers. - `ly.img.vector.shape` — Shapes and arrows. - `ly.img.filter` — LUT and duotone color filters. - `ly.img.color.palette` — Default color palette. - `ly.img.effect` — Effects. - `ly.img.blur` — Blurs. - `ly.img.typeface` — Typefaces. - `ly.img.crop.presets` — Crop presets. - `ly.img.page.presets` — Page resize presets. - `ly.img.text`, `ly.img.text.styles`, `ly.img.text.curves` — Text style presets. - `ly.img.text.components` — Text design component library. ## Register Sample Content Sources The archive also ships sample content — images, videos, audio, and templates — registered the same way. These are meant for development and prototyping; replace them with your own content sources in production. ```swift highlight-serveAssets-demoSourceIDs private let serveAssetsDemoSourceIDs = [ "ly.img.image", "ly.img.video", "ly.img.audio", "ly.img.templates", "ly.img.templates.premium", ] ``` ```swift highlight-serveAssets-registerDemo for id in serveAssetsDemoSourceIDs { _ = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent(id).appendingPathComponent("content.json"), ) } ``` The sample content sources are: - `ly.img.image` — Sample images. - `ly.img.video` — Sample videos. - `ly.img.audio` — Sample audio. - `ly.img.templates` — Sample design templates. - `ly.img.templates.premium` — Premium sample design templates. ## Point the Base URL at Your Assets Set `baseURL` to wherever you copied the assets, then register the sources exactly as above. For assets on your own server or CDN, use an absolute URL pointing at the folder that contains the per-source directories: ```swift highlight-serveAssets-remoteBaseURL let baseURL = URL(string: "https://cdn.your.custom.domain/assets")! for id in serveAssetsDefaultSourceIDs { _ = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent(id).appendingPathComponent("content.json"), ) } ``` For assets bundled with your app, resolve the `.bundle` URL you added to your app target: ```swift highlight-serveAssets-bundleBaseURL guard let baseURL = Bundle.main.url(forResource: "IMGLYAssets", withExtension: "bundle") else { return } for id in serveAssetsDefaultSourceIDs { _ = try await engine.asset.addLocalAssetSourceFromJSON( baseURL.appendingPathComponent(id).appendingPathComponent("content.json"), ) } ``` ## Customize Which Assets Load To register only a subset of a source's assets, pass ID patterns to the `matcher:` parameter of `addLocalAssetSourceFromJSON(_:matcher:)`. Patterns support the `*` wildcard, and an asset is included if it matches any pattern — for example, `matcher: ["ly.img.sticker.emoji.*"]` registers only the emoji stickers. For deeper changes — curating your own collection, renaming assets, or adjusting their metadata — edit the `content.json` manifests directly. See the [Asset Content JSON Schema](https://img.ly/docs/cesdk/mac-catalyst/import-media/content-json-schema-a7b3d2/) guide for the manifest format. ## Configure Engine-Level Assets The engine also loads font fallback files (for Unicode character coverage) and the emoji font separately from the asset sources. Point the `basePath` setting at your location so they load from there too: ```swift highlight-serveAssets-engineLevelAssets try engine.editor.setSettingString("basePath", value: baseURL.absoluteString) ``` This setting affects: - **Font fallback files** — Used when text contains characters not covered by the selected font. Located at `{basePath}/fonts/font-{index}.ttf`. - **Emoji font** — The default emoji font (`NotoColorEmoji.ttf`). Located at `{basePath}/emoji/NotoColorEmoji.ttf`. Both the `fonts/` and `emoji/` directories are included in the `imgly-assets.zip` download, so once the assets are at your `basePath` location the engine resolves them automatically. > **Note:** On iOS, when you embed the prebuilt editor UI, pass your `baseURL` to `EngineSettings` instead of setting `basePath` yourself. The editor initializes the engine's `basePath` from it before running the `onCreate` callback, where you register the asset sources shown above. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.asset.addLocalAssetSourceFromJSON(_:matcher:)` | Register an asset source by loading its `content.json` manifest from a URL. Pass `matcher` ID patterns to filter which assets load. Returns the source ID. | | `engine.editor.setSettingString("basePath", value:)` | Set the base URL for font fallback files and the emoji font. | ## Next Steps - [Configuration](#broken-link-2c1c3d) — Pass `baseURL` to `EngineSettings` so the editor initializes `basePath` for you before `onCreate` runs. - [Assets](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) — How asset sources and assets fit together. - [Insert Shapes or Stickers](https://img.ly/docs/cesdk/mac-catalyst/insert-media/shapes-or-stickers-20ac68/) — Query and apply assets from a registered source. --- ## 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: "Settings" description: "Explore all configurable editor settings and learn how to read, update, and observe them via the Settings API." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/" --- > 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/) > [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) --- ```swift file=@cesdk_swift_examples/engine-guides-settings/Settings.swift reference-only import Foundation import IMGLYEngine @MainActor func settings(engine: Engine) async throws { let allSettings = engine.editor.findAllSettings() let settingType = try engine.editor.getSettingType("doubleClickSelectionMode") try engine.editor.setSettingBool("doubleClickToCropEnabled", value: true) let cropEnabled = try engine.editor.getSettingBool("doubleClickToCropEnabled") try engine.editor.setSettingInt("maxImageSize", value: 4096) let maxImageSize = try engine.editor.getSettingInt("maxImageSize") try engine.editor.setSettingFloat("positionSnappingThreshold", value: 2.0) let snappingThreshold = try engine.editor.getSettingFloat("positionSnappingThreshold") try engine.editor.setSettingString("page/title/separator", value: " | ") let separator = try engine.editor.getSettingString("page/title/separator") try engine.editor.setSettingColor("highlightColor", color: .rgba(r: 1, g: 0, b: 1, a: 1)) let highlightColor: Color = try engine.editor.getSettingColor("highlightColor") let modes = try engine.editor.getSettingEnumOptions("doubleClickSelectionMode") try engine.editor.setSettingEnum("doubleClickSelectionMode", value: "Direct") let selectionMode = try engine.editor.getSettingEnum("doubleClickSelectionMode") let settingsTask = Task { for await _ in engine.editor.onSettingsChanged { print("Editor settings have changed") } } let role = try engine.editor.getRole() try engine.editor.setRole("Adopter") let roleTask = Task { for await newRole in engine.editor.onRoleChanged { print("Role changed to \(newRole)") } } _ = (allSettings, settingType, cropEnabled, maxImageSize, snappingThreshold, separator, highlightColor, modes, selectionMode, role) settingsTask.cancel() roleTask.cancel() } ``` Explore all configurable editor settings and learn how to read, update, and observe them via the Settings API. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-settings) Settings are configuration values that control CE.SDK editor behavior without modifying scene content. They are accessed via key paths (e.g., `page/title/show`) and support multiple types — `Bool`, `Int`, `Float`, `String`, `Color`, and `Enum`. Use settings to customize visual appearance, interaction behavior, resource paths, and feature toggles. ## When to Change Settings Settings can be changed at any time after engine initialization, but they fall into two categories based on when they should be modified. ### Initialization-Only Settings Some settings should only be set once during or immediately after engine initialization. Changing them later may have no effect or cause unexpected behavior: - `license` - The license key validates on startup; changing it later has no effect - `basePath` - The base URL for resolving assets; should be set before loading any resources - `defaultFontFileUri` / `defaultEmojiFontFileUri` - Default fonts used when no font is specified; should be set early - `maxImageSize` - Memory limit for images; changing mid-session won't affect already-loaded images ### Runtime Settings Most settings can be changed at any time and take effect immediately: - **Visual appearance**: `highlightColor`, `snappingGuideColor`, `cropOverlayColor`, `page/title/color` - **Interaction behavior**: `doubleClickToCropEnabled`, `doubleClickSelectionMode`, `touch/*`, `mouse/*` - **Control gizmos**: `controlGizmo/showResizeHandles`, `controlGizmo/showRotateHandles` - **Page display**: `page/title/show`, `page/dimOutOfPageAreas`, `page/title/separator` - **Feature toggles**: `blockAnimations/enabled`, `useSystemFontFallback`, `forceSystemEmojis` - **Snapping thresholds**: `positionSnappingThreshold`, `rotationSnappingThreshold` These runtime settings are commonly used to adapt the editor UI to different modes, user preferences, or workflow states. ## Using the Settings API ### Discover Available Settings Use `findAllSettings()` to enumerate every available setting key, and `getSettingType(_:)` to inspect the `PropertyType` of any individual keypath: ```swift highlight-settings-discover let allSettings = engine.editor.findAllSettings() let settingType = try engine.editor.getSettingType("doubleClickSelectionMode") ``` ### Read and Write Settings The Swift binding exposes one read/write pair per value type. Pick the accessor that matches the setting's type — boolean toggles use `setSettingBool`/`getSettingBool`, numeric settings use the `Int` or `Float` variants, string-typed settings (including URIs and the `license` key) use the `String` variants, color settings use the `Color`-typed `setSettingColor(_:color:)` / `getSettingColor(_:)` pair, and enum settings use the string-based `setSettingEnum`/`getSettingEnum` pair together with `getSettingEnumOptions(_:)` to discover the valid values for a given key: ```swift highlight-settings-read-write try engine.editor.setSettingBool("doubleClickToCropEnabled", value: true) let cropEnabled = try engine.editor.getSettingBool("doubleClickToCropEnabled") try engine.editor.setSettingInt("maxImageSize", value: 4096) let maxImageSize = try engine.editor.getSettingInt("maxImageSize") try engine.editor.setSettingFloat("positionSnappingThreshold", value: 2.0) let snappingThreshold = try engine.editor.getSettingFloat("positionSnappingThreshold") try engine.editor.setSettingString("page/title/separator", value: " | ") let separator = try engine.editor.getSettingString("page/title/separator") try engine.editor.setSettingColor("highlightColor", color: .rgba(r: 1, g: 0, b: 1, a: 1)) let highlightColor: Color = try engine.editor.getSettingColor("highlightColor") let modes = try engine.editor.getSettingEnumOptions("doubleClickSelectionMode") try engine.editor.setSettingEnum("doubleClickSelectionMode", value: "Direct") let selectionMode = try engine.editor.getSettingEnum("doubleClickSelectionMode") ``` `getSettingColor(_:)` ships two overloads — a legacy `RGBA`-returning form and the current `Color`-returning form. Annotate the read site with `let value: Color = …` (as in the snippet above) to select the non-deprecated overload; without the annotation the call is ambiguous and won't compile. ### Subscribe to Settings Changes `onSettingsChanged` is an `AsyncStream` that yields whenever any setting changes. Iterate it from a `Task` and cancel the task to stop observing. A Combine variant, `onSettingsChangedPublisher`, is also available for callers using Combine: ```swift highlight-settings-observe let settingsTask = Task { for await _ in engine.editor.onSettingsChanged { print("Editor settings have changed") } } ``` ### Role Management Roles apply predefined defaults for scopes and settings. The engine ships four built-in roles: - `Creator` (default) — every global scope set to `Allow`. - `Adopter` — defers scope decisions to per-block settings while still allowing `editor.add`. - `Presenter` — denies every global scope; intended for guided playback. - `Viewer` — denies every global scope; intended for read-only previews. Use `getRole()` to read the current role, `setRole(_:)` to apply a new one, and the `onRoleChanged` async stream (or `onRoleChangedPublisher` for Combine) to react when the role changes: ```swift highlight-settings-role let role = try engine.editor.getRole() try engine.editor.setRole("Adopter") let roleTask = Task { for await newRole in engine.editor.onRoleChanged { print("Role changed to \(newRole)") } } ``` ## Available Settings ## Settings Type Editor Settings This section describes the all available editor settings. | Property | Type | Default | Description | | -------------------------------------------------- | -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `archival/bundleOnlyUsedFontVariants` | `Bool` | `false` | When enabled, `saveSceneToArchive` and `saveBlocksToArchive` bundle only the font variants actually referenced by text blocks. When disabled (default), every variant of each referenced typeface is bundled so the loaded scene can freely switch fonts without re-fetching assets. | | `basePath` | `String` | `"some-base-path"` | The root directory for resolving relative paths and `bundle://` URIs (on platforms that don't offer bundles.). Also used as the base URL for loading font fallback files and the default emoji font (when self-hosting assets). | | `blockAnimations/enabled` | `Bool` | `true` | Whether animations should be enabled or not. | | `borderOutlineColor` | `Color` | `{"r":0,"g":0,"b":0,"a":1}` | The border outline color. | | `camera/clamping/overshootMode` | `Enum` | `"Reverse"` | Controls what happens when the clamp area is smaller than the viewport. Center: the clamp area is centered in the viewport. Reverse: the clamp area can move inside the viewport until it hits the edges., Possible values: `"Center"`, `"Reverse"` | | `clampThumbnailTextureSizes` | `Bool` | `true` | Whether to clamp thumbnail texture sizes to reduce memory usage. | | `clearColor` | `Color` | `{"r":0,"g":0,"b":0,"a":0}` | The color with which the render target is cleared before scenes get rendered. Only used while renderMode == RenderMode::Preview. | | `colorMaskingSettings/maskColor` | `Color` | `{"r":1,"g":1,"b":1,"a":1}` | The current mask color. Defaults to white, which disabled all masking. | | `controlGizmo/blockScaleDownLimit` | `Float` | `8` | Scale-down limit for blocks in screen pixels when scaling them with the gizmos or with touch gestures. The limit is ensured to be at least 0.1 to prevent scaling to size zero. | | `controlGizmo/dynamicMoveHandleVisibility` | `Bool` | `true` | Deprecated: prefer `controlGizmo/moveHandleVisibility`. Whether the move handle visibility is dynamic based on block size. When enabled (default), the move handle only appears when the block is small enough that resize handles would cover the interaction area. Setting it to false is equivalent to `moveHandleVisibility: 'always'`. | | `controlGizmo/moveHandleVisibility` | `Enum` | `"auto"` | Controls when the standalone move handle is shown for a selected block. `'auto'` (default) shows it only when the block is too small for the resize handles to be usable; `'always'` shows it regardless of block size and also while the block is in text edit (input) mode, so it can be repositioned while typing; `'never'` hides it. `'always'` does not apply in crop edit mode, which has its own handles. Supersedes the deprecated `controlGizmo/showMoveHandles` and `controlGizmo/dynamicMoveHandleVisibility` booleans., Possible values: `"auto"`, `"always"`, `"never"` | | `controlGizmo/resizeHandlesVisibility` | `Enum` | `"auto"` | Controls when the non-proportional edge (resize) handles are shown. `'auto'` (default) shows them in transform edits; `'always'` also shows them while the block is in text edit (input) mode; `'never'` hides them. `'always'` does not apply in crop edit mode, which has its own handles. Supersedes the deprecated `controlGizmo/showResizeHandles` boolean., Possible values: `"auto"`, `"always"`, `"never"` | | `controlGizmo/rotateHandlesVisibility` | `Enum` | `"auto"` | Controls when the rotation handle is shown. `'auto'` (default) shows it in transform edits; `'always'` also shows it while the block is in text edit (input) mode; `'never'` hides it. `'always'` does not apply in crop edit mode, which has its own handles. Supersedes the deprecated `controlGizmo/showRotateHandles` boolean., Possible values: `"auto"`, `"always"`, `"never"` | | `controlGizmo/scaleHandlesVisibility` | `Enum` | `"auto"` | Controls when the proportional corner (scale) handles are shown. `'auto'` (default) shows them in transform edits; `'always'` also shows them while the block is in text edit (input) mode; `'never'` hides them. `'always'` does not apply in crop edit mode, which has its own handles. Supersedes the deprecated `controlGizmo/showScaleHandles` boolean., Possible values: `"auto"`, `"always"`, `"never"` | | `controlGizmo/showCropHandles` | `Bool` | `true` | Whether or not to show the handles to adjust the crop area during crop mode. | | `controlGizmo/showCropScaleHandles` | `Bool` | `true` | Whether or not to display the outer handles that scale the full image during crop. | | `controlGizmo/showMoveHandles` | `Bool` | `true` | Deprecated: prefer `controlGizmo/moveHandleVisibility`. Master on/off switch for the move handle; setting it to false is equivalent to `moveHandleVisibility: 'never'`. | | `controlGizmo/showResizeHandles` | `Bool` | `true` | Deprecated: prefer `controlGizmo/resizeHandlesVisibility`. Whether or not to display the non-proportional resize handles (edge handles). Setting it to false is equivalent to `resizeHandlesVisibility: 'never'`. | | `controlGizmo/showRotateHandles` | `Bool` | `true` | Deprecated: prefer `controlGizmo/rotateHandlesVisibility`. Whether or not to show the rotation handle. Setting it to false is equivalent to `rotateHandlesVisibility: 'never'`. | | `controlGizmo/showScaleHandles` | `Bool` | `true` | Deprecated: prefer `controlGizmo/scaleHandlesVisibility`. Whether or not to display the proportional scale handles (corner handles). Setting it to false is equivalent to `scaleHandlesVisibility: 'never'`. | | `cropOverlayColor` | `Color` | `{"r":0,"g":0,"b":0,"a":0.39}` | Color of the dimming overlay that's added in crop mode. | | `defaultEmojiFontFileUri` | `String` | `""` | URI of default font file for emojis. | | `defaultFontFileUri` | `String` | `"bundle://ly.img.cesdk/fonts/imgly_font_inter_semibold.otf"` | URI of default font file. This font file is the default everywhere unless overriden in specific settings. | | `doubleClickSelectionMode` | `Enum` | `"Hierarchical"` | The current mode of selection on double-click., Possible values: `"Direct"`, `"Hierarchical"` | | `doubleClickToCropEnabled` | `Bool` | `true` | Whether double clicking on an image element should switch into the crop editing mode. | | `errorStateColor` | `Color` | `{"r":1,"g":1,"b":1,"a":0.7}` | The error state color for design blocks. | | `fallbackFontUri` | `String` | `""` | The URI of the fallback font to use for text that is missing certain characters. | | `forceSystemEmojis` | `Bool` | `true` | Whether the system emojis should be used for text. | | `grid/color` | `Color` | `{"r":0.518,"g":0.518,"b":0.518,"a":0.3}` | Color of the grid lines. | | `grid/enabled` | `Bool` | `false` | Whether the background grid is shown on pages. | | `grid/snapEnabled` | `Bool` | `false` | Whether elements should snap to grid lines when dragged. | | `grid/spacingX` | `Float` | `10` | Horizontal spacing between vertical grid lines in design units. | | `grid/spacingY` | `Float` | `10` | Vertical spacing between horizontal grid lines in design units. | | `handleFillColor` | `Color` | `{"r":1,"g":1,"b":1,"a":1}` | The fill color for handles. | | `highlightColor` | `Color` | `{"r":0.2,"g":0.333,"b":1,"a":1}` | Color of the selection, hover, and group frames and for the handle outlines for non-placeholder elements. | | `license` | `String` | `""` | A valid license string in JWT format. | | `listIndentPerLevel` | `Float` | `0.75` | Width of each list indentation level in EM units. | | `maxImageSize` | `Int` | `4096` | The maximum size at which images are loaded into the engine. Images that exceed this size are down-scaled prior to rendering. Reducing this size further reduces the memory footprint. | | `maxPreviewResolution` | `Int` | `-1` | The maximum dimension (width or height) in physical pixels for preview rendering. When greater than 0, the scene is rendered at reduced resolution and upscaled for improved performance. Does not affect exports. Set to -1 to disable (default). | | `mouse/enableScroll` | `Bool` | `true` | Whether the engine processes mouse scroll events. | | `mouse/enableZoom` | `Bool` | `true` | Whether the engine processes mouse zoom events. | | `page/allowCropInteraction` | `Bool` | `true` | If crop interaction (by handles and gestures) should be possible when the enabled arrangements allow resizing. | | `page/allowMoveInteraction` | `Bool` | `false` | If move interaction (by handles and gestures) should be possible when the enabled arrangements allow moving and if the page layout is not controlled by the scene. | | `page/allowResizeInteraction` | `Bool` | `false` | If a resize interaction (by handles and gestures) should be possible when the enabled arrangements allow resizing. | | `page/allowRotateInteraction` | `Bool` | `false` | If rotation interaction (by handles and gestures) should be possible when the enabled arrangements allow rotation and if the page layout is not controlled by the scene. | | `page/allowShapeChange` | `Bool` | `false` | Whether pages support non-rectangular shapes. When false, `supportsShape` returns false for pages. | | `page/dimOutOfPageAreas` | `Bool` | `true` | Whether the opacity of the region outside of all pages should be reduced. | | `page/flipDimensionsOn90DegreeCropRotation` | `Bool` | `false` | Whether rotating the crop by 90 degrees should swap the block's width and height, causing the page aspect ratio to rotate with the content. Defaults to false. | | `page/highlightDropTarget` | `Bool` | `false` | Whether to highlight the page under a dragged element as a drop target. | | `page/highlightWhenCropping` | `Bool` | `false` | Whether highlighting should be automatically enabled on the current page when entering crop mode. | | `page/innerBorderColor` | `Color` | `{"r":0,"g":0,"b":0,"a":0}` | Color of the inner frame around the page. | | `page/marginFillColor` | `Color` | `{"r":0.79,"g":0.12,"b":0.4,"a":0.1}` | Color filled into the bleed margins of pages. | | `page/marginFrameColor` | `Color` | `{"r":0.79,"g":0.12,"b":0.4,"a":0.15}` | Color of frame around the bleed margin area of the pages. | | `page/marqueeSelectOnBodyDrag` | `Bool` | `false` | When enabled, a click+drag that starts on the page body performs a marquee selection of the blocks inside the page instead of moving the page. The page can still be moved by dragging its title (when visible in free layout) or by holding the command key (macOS) / control key (Windows/Linux) while clicking and dragging on the page body. Has no effect when the page is not movable (see `page/allowMoveInteraction` and scene layout constraints). | | `page/moveChildrenWhenCroppingFill` | `Bool` | `false` | Whether the children of the page should be transformed to match their old position relative to the page fill when a page fill is cropped. | | `page/outerBorderColor` | `Color` | `{"r":1,"g":1,"b":1,"a":0}` | Color of the outer frame around the page. | | `page/reparentBlocksToSceneWhenOutOfPage` | `Bool` | `false` | Whether blocks should be reparented to the scene when dragged outside all pages, and reparented back to a page when dragged over one. | | `page/restrictPageSelectionToBorderAndTitle` | `Bool` | `false` | When enabled, the page can only be selected by clicking on its title (when shown in free layout) or near its border. Clicks inside the page body no longer select the page; the click falls through to whatever block sits underneath. Independent of `page/marqueeSelectOnBodyDrag`. | | `page/restrictResizeInteractionToFixedAspectRatio` | `Bool` | `false` | If the resize interaction should be restricted to fixed aspect ratio resizing. | | `page/selectWhenNoBlocksSelected` | `Bool` | `false` | Whether the page should automatically be selected when no blocks are selected. | | `page/title/appendPageName` | `Bool` | `true` | Whether to append the page name to the title if a page name is set even if the name is not specified in the template or the template is not shown. | | `page/title/canEdit` | `Bool` | `false` | Whether double-clicking a page title enters text edit mode to rename the page. | | `page/title/color` | `Color` | `{"r":1,"g":1,"b":1,"a":1}` | Color of page titles visible in preview mode, can change with different themes. | | `page/title/fontFileUri` | `String` | `"bundle://ly.img.cesdk/fonts/imgly_font_inter_semibold.otf"` | Font of page titles. | | `page/title/separator` | `String` | `"-"` | Title label separator between the page number and the page name. | | `page/title/show` | `Bool` | `true` | Whether to show titles above each page. | | `page/title/showOnSinglePage` | `Bool` | `true` | Whether to hide the the page title when only a single page is given. | | `page/title/showPageTitleTemplate` | `Bool` | `true` | Whether to include the default page title from `page.titleTemplate`. | | `pageHighlightColor` | `Color` | `{"r":0.5,"g":0.5,"b":0.5,"a":0.2}` | Color of the outline of each page. | | `placeholderControls/showButton` | `Bool` | `true` | Show the placeholder button. | | `placeholderControls/showOverlay` | `Bool` | `true` | Show the overlay pattern. | | `placeholderHighlightColor` | `Color` | `{"r":0.77,"g":0.06,"b":0.95,"a":1}` | Color of the selection, hover, and group frames and for the handle outlines for placeholder elements. | | `playback/showAllBlocks` | `Bool` | `false` | When enabled, every block stays visible regardless of the current playback time, instead of being culled outside its time offset/duration. No effect on export. | | `positionSnappingThreshold` | `Float` | `4` | Position snapping threshold in screen space. | | `progressColor` | `Color` | `{"r":1,"g":1,"b":1,"a":0.7}` | The progress indicator color. | | `rotationSnappingGuideColor` | `Color` | `{"r":1,"g":0.004,"b":0.361,"a":1}` | Color of the rotation snapping guides. | | `rotationSnappingThreshold` | `Float` | `0.15` | Rotation snapping threshold in radians. | | `showBuildVersion` | `Bool` | `false` | Show the build version on the canvas. | | `snappingGuideColor` | `Color` | `{"r":1,"g":0.004,"b":0.361,"a":1}` | Color of the position snapping guides. | | `textVariableHighlightColor` | `Color` | `{"r":0.7,"g":0,"b":0.7,"a":1}` | Color of the text variable highlighting borders. | | `touch/dragStartCanSelect` | `Bool` | `true` | Whether dragging an element requires selecting it first. When not set, elements can be directly dragged. | | `touch/pinchAction` | `Enum` | `"Scale"` | The action to perform when a pinch gesture is performed., Possible values: `"None"`, `"Zoom"`, `"Scale"`, `"Auto"`, `"Dynamic"` | | `touch/rotateAction` | `Enum` | `"Rotate"` | Whether or not the two finger turn gesture can rotate selected elements., Possible values: `"None"`, `"Rotate"` | | `touch/singlePointPanning` | `Bool` | `true` | Whether or not dragging on the canvas should move the camera (scrolling). When not set, the scroll bars have to be used. | | `upload/supportedMimeTypes` | `String` | `""` | The MIME types supported for file uploads. | | `useSystemFontFallback` | `Bool` | `true` | Whether the IMG.LY hosted font fallback is used for fonts that are missing certain characters, covering most of the unicode range. | ### GlobalScopes | Member | Type | Default | Description | | ---------- | ------- | ------- | -------------------------------- | | text | `Scope` | `Allow` | Scope for text operations. | | fill | `Scope` | `Allow` | Scope for fill operations. | | stroke | `Scope` | `Allow` | Scope for stroke operations. | | shape | `Scope` | `Allow` | Scope for shape operations. | | layer | `Scope` | `Allow` | Scope for layer operations. | | appearance | `Scope` | `Allow` | Scope for appearance operations. | | lifecycle | `Scope` | `Allow` | Scope for lifecycle operations. | | editor | `Scope` | `Allow` | Scope for editor operations. | ## Next Steps - [Configuration](#broken-link-2c1c3d) — Pass the init-time values (`license`, `userID`, `baseURL`) via `EngineSettings` before the runtime settings above take effect. - [Serve Assets From Your Server](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) — Host the assets that `basePath` resolves against and register the default asset sources against your own infrastructure. --- ## 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 and Edit Shapes" description: "Draw custom vector shapes, combine them with boolean operations, and insert QR codes into your designs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/shapes-9f1b2c/" --- > 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 Shapes](https://img.ly/docs/cesdk/mac-catalyst/shapes-9f1b2c/) --- --- ## Related Pages - [Create Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-shapes-64acc0/) - Create and configure geometric shapes programmatically using the Engine API in CE.SDK. - [Edit Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) - Edit graphic block shapes programmatically — replace geometry, modify shape-specific properties, change fills and strokes, transform, combine, and group. - [Combine Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/combine-2a9e26/) - Combine multiple shapes using boolean operations to create custom compound designs. - [Insert QR Code](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/insert-qr-code-b6cc53/) - Generate a QR code with Core Image and insert it into a scene as an image fill, with positioning, sizing, and optional metadata for later updates. --- ## 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 and Edit Stickers" description: "Create and customize stickers using image fills for icons, logos, emoji, and multi-color graphics." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/stickers-3d4e5f/" --- > 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 Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-3d4e5f/) --- --- ## Related Pages - [Create Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-stickers-cc46e5/) - Create stickers in CE.SDK using image fills for icons, logos, emoji, and multi-color graphics - [Edit Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-stickers-609679/) - Modify stickers programmatically — swap the image, transform, restyle with shadows or strokes, and duplicate. - [Create Cutout](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-cutout-384be3/) - Create cutouts from images or shapes by masking or removing specific areas. --- ## 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: "Combine Shapes" description: "Combine multiple shapes using boolean operations to create custom compound designs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/combine-2a9e26/" --- > 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 Shapes](https://img.ly/docs/cesdk/mac-catalyst/shapes-9f1b2c/) > [Combine Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/combine-2a9e26/) --- ```swift file=@cesdk_swift_examples/engine-guides-bool-ops/BoolOps.swift reference-only import Foundation import IMGLYEngine @MainActor func boolOps(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 // Union demo: three overlapping circles in the top-left quadrant. let circle1 = try engine.block.create(.graphic) try engine.block.setShape(circle1, shape: engine.block.createShape(.ellipse)) let fill1 = try engine.block.createFill(.color) try engine.block.setColor( fill1, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.35, b: 0.35, a: 1.0), ) try engine.block.setFill(circle1, fill: fill1) try engine.block.setPositionX(circle1, value: 120) try engine.block.setPositionY(circle1, value: 90) try engine.block.setWidth(circle1, value: 110) try engine.block.setHeight(circle1, value: 110) try engine.block.appendChild(to: page, child: circle1) let circle2 = try engine.block.create(.graphic) try engine.block.setShape(circle2, shape: engine.block.createShape(.ellipse)) let fill2 = try engine.block.createFill(.color) try engine.block.setColor( fill2, property: "fill/color/value", color: .rgba(r: 0.30, g: 0.80, b: 0.45, a: 1.0), ) try engine.block.setFill(circle2, fill: fill2) try engine.block.setPositionX(circle2, value: 190) try engine.block.setPositionY(circle2, value: 90) try engine.block.setWidth(circle2, value: 110) try engine.block.setHeight(circle2, value: 110) try engine.block.appendChild(to: page, child: circle2) let circle3 = try engine.block.create(.graphic) try engine.block.setShape(circle3, shape: engine.block.createShape(.ellipse)) let fill3 = try engine.block.createFill(.color) try engine.block.setColor( fill3, property: "fill/color/value", color: .rgba(r: 0.25, g: 0.55, b: 0.95, a: 1.0), ) try engine.block.setFill(circle3, fill: fill3) try engine.block.setPositionX(circle3, value: 155) try engine.block.setPositionY(circle3, value: 140) try engine.block.setWidth(circle3, value: 130) try engine.block.setHeight(circle3, value: 130) try engine.block.appendChild(to: page, child: circle3) if try engine.block.isCombinable([circle1, circle2, circle3]) { print("Blocks are combinable") } let unionResult = try engine.block.combine( [circle1, circle2, circle3], booleanOperation: .union, ) try engine.block.setName(unionResult, name: "Union") try await engine.captureGuide(page, label: "after-union") // Difference demo: a star punched out of an image in the top-right quadrant. 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.setPositionX(image, value: 460) try engine.block.setPositionY(image, value: 60) try engine.block.setWidth(image, value: 280) try engine.block.setHeight(image, value: 180) try engine.block.appendChild(to: page, child: image) let cutoutStar = try engine.block.create(.graphic) try engine.block.setShape(cutoutStar, 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.0, g: 0.0, b: 0.0, a: 1.0), ) try engine.block.setFill(cutoutStar, fill: starFill) try engine.block.setPositionX(cutoutStar, value: 520) try engine.block.setPositionY(cutoutStar, value: 80) try engine.block.setWidth(cutoutStar, value: 160) try engine.block.setHeight(cutoutStar, value: 140) try engine.block.appendChild(to: page, child: cutoutStar) // Load image resources before combining media-backed blocks so the // resulting image fill is ready for rendering. try await engine.block.forceLoadResources([image]) // Difference subtracts upper blocks from the bottom-most base block and // inherits the base block's fill, so send the image to the back first. try engine.block.sendToBack(image) let differenceResult = try engine.block.combine( [image, cutoutStar], booleanOperation: .difference, ) try engine.block.setName(differenceResult, name: "Difference") try await engine.captureGuide(page, label: "after-difference") // Intersection demo: two overlapping circles in the bottom-left quadrant. let lensA = try engine.block.create(.graphic) try engine.block.setShape(lensA, shape: engine.block.createShape(.ellipse)) let lensFillA = try engine.block.createFill(.color) try engine.block.setColor( lensFillA, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.80, b: 0.25, a: 1.0), ) try engine.block.setFill(lensA, fill: lensFillA) try engine.block.setPositionX(lensA, value: 60) try engine.block.setPositionY(lensA, value: 360) try engine.block.setWidth(lensA, value: 200) try engine.block.setHeight(lensA, value: 200) try engine.block.appendChild(to: page, child: lensA) let lensB = try engine.block.create(.graphic) try engine.block.setShape(lensB, shape: engine.block.createShape(.ellipse)) let lensFillB = try engine.block.createFill(.color) try engine.block.setColor( lensFillB, property: "fill/color/value", color: .rgba(r: 0.30, g: 0.70, b: 0.85, a: 1.0), ) try engine.block.setFill(lensB, fill: lensFillB) try engine.block.setPositionX(lensB, value: 180) try engine.block.setPositionY(lensB, value: 360) try engine.block.setWidth(lensB, value: 200) try engine.block.setHeight(lensB, value: 200) try engine.block.appendChild(to: page, child: lensB) // Intersection inherits the bottom-most block's fill, so send the block // whose fill should survive to the back before combining. try engine.block.sendToBack(lensA) let intersectionResult = try engine.block.combine( [lensA, lensB], booleanOperation: .intersection, ) try engine.block.setName(intersectionResult, name: "Intersection") try await engine.captureGuide(page, label: "after-intersection") // XOR demo: two overlapping circles in the bottom-right quadrant. let xorA = try engine.block.create(.graphic) try engine.block.setShape(xorA, shape: engine.block.createShape(.ellipse)) let xorFillA = try engine.block.createFill(.color) try engine.block.setColor( xorFillA, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.40, b: 0.70, a: 1.0), ) try engine.block.setFill(xorA, fill: xorFillA) try engine.block.setPositionX(xorA, value: 460) try engine.block.setPositionY(xorA, value: 360) try engine.block.setWidth(xorA, value: 200) try engine.block.setHeight(xorA, value: 200) try engine.block.appendChild(to: page, child: xorA) let xorB = try engine.block.create(.graphic) try engine.block.setShape(xorB, shape: engine.block.createShape(.ellipse)) let xorFillB = try engine.block.createFill(.color) try engine.block.setColor( xorFillB, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.60, b: 0.20, a: 1.0), ) try engine.block.setFill(xorB, fill: xorFillB) try engine.block.setPositionX(xorB, value: 580) try engine.block.setPositionY(xorB, value: 360) try engine.block.setWidth(xorB, value: 200) try engine.block.setHeight(xorB, value: 200) try engine.block.appendChild(to: page, child: xorB) // XOR inherits the top-most block's fill. let xorResult = try engine.block.combine( [xorA, xorB], booleanOperation: .xor, ) try engine.block.setName(xorResult, name: "XOR") try await engine.captureGuide(page, label: "hero") } ``` Combine multiple shapes using boolean operations to create custom compound designs programmatically. ![Boolean operations preview showing Union, Difference, Intersection, and XOR results arranged in four quadrants of the page.](./assets/swift-based.hero.webp) > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-bool-ops) CE.SDK provides four boolean operations for graphic and text blocks: Union, Difference, Intersection, and XOR. Use them to merge simple primitives, create cutouts, isolate overlapping areas, or remove overlaps from a compound shape. This guide covers checking combinability, applying the four operations with Swift's `BooleanOperation` enum, controlling fill inheritance through block order, and avoiding common scope issues. ## Understanding Boolean Operations Boolean operations create a new block from the geometry of multiple input blocks. The result replaces the input blocks when the required scopes allow CE.SDK to duplicate and destroy them. | Operation | Swift case | Result | | --- | --- | --- | | Union | `BooleanOperation.union` | Merges all block areas into one compound shape | | Difference | `BooleanOperation.difference` | Subtracts upper blocks from the bottom-most base block | | Intersection | `BooleanOperation.intersection` | Keeps only the areas where all blocks overlap | | XOR | `BooleanOperation.xor` | Keeps non-overlapping areas and removes intersections | For Union and XOR, the new block inherits the fill from the top-most block. For Difference and Intersection, it inherits the fill from the bottom-most block. The operation order follows visual stacking order, not the order of the block IDs in the array. Reorder blocks with `engine.block.bringToFront(_:)` or `engine.block.sendToBack(_:)` before combining when fill inheritance matters. > **Note:** **Only these block types can be combined*** Graphic blocks > * Text blocks ## Checking Combinability Before combining blocks, call `engine.block.isCombinable(_:)`. It returns `true` only when the array contains at least two compatible graphic or text blocks on the same page and each block has the `"lifecycle/duplicate"` scope enabled. ```swift highlight-bool-ops-check-combinability if try engine.block.isCombinable([circle1, circle2, circle3]) { print("Blocks are combinable") } ``` Use the same array of blocks for the later `combine(_:booleanOperation:)` call so the checked selection and the mutated selection cannot drift. ## Combining with Union Union merges several blocks into one compound outline. In this sample, three overlapping circles become a single block and inherit the blue fill from the top-most circle. ```swift highlight-bool-ops-combine-union let unionResult = try engine.block.combine( [circle1, circle2, circle3], booleanOperation: .union, ) try engine.block.setName(unionResult, name: "Union") ``` `engine.block.combine(_:booleanOperation:)` returns the newly created block. The example assigns a name with `engine.block.setName(_:name:)` so the result is easy to identify in the scene hierarchy. Use Union for merged logos, compound icons, and shapes built from simple primitives. ## Combining with Difference Difference subtracts upper blocks from the bottom-most base block. Place the base block behind the subtracting blocks before combining so the result keeps the intended fill. ```swift highlight-bool-ops-combine-difference // Load image resources before combining media-backed blocks so the // resulting image fill is ready for rendering. try await engine.block.forceLoadResources([image]) // Difference subtracts upper blocks from the bottom-most base block and // inherits the base block's fill, so send the image to the back first. try engine.block.sendToBack(image) let differenceResult = try engine.block.combine( [image, cutoutStar], booleanOperation: .difference, ) try engine.block.setName(differenceResult, name: "Difference") ``` The sample places an image block behind a star-shaped graphic block, then removes the star from the image. The combined block keeps the image's fill and has a star-shaped hole punched through it. `engine.block.forceLoadResources(_:)` loads the image fill before combining so the operation can resolve its shape. Without it, the engine has no pixel data resolved for the image block and the difference operation may produce unexpected results. ## Combining with Intersection Intersection keeps only the area shared by all selected blocks. The result inherits the bottom-most block's fill. The sample uses two overlapping circles to create a lens-shaped result. ```swift highlight-bool-ops-combine-intersection // Intersection inherits the bottom-most block's fill, so send the block // whose fill should survive to the back before combining. try engine.block.sendToBack(lensA) let intersectionResult = try engine.block.combine( [lensA, lensB], booleanOperation: .intersection, ) try engine.block.setName(intersectionResult, name: "Intersection") ``` Use Intersection for overlap effects, lens shapes, and geometric masks. ## Combining with XOR XOR (exclusive OR) keeps the non-overlapping areas of the selected blocks and removes the area where they overlap. Two overlapping circles, for example, become a single block that contains the two outer crescents and a hole where they crossed — the same "donut" shape as cutting the intersection out of their union. ```swift highlight-bool-ops-combine-xor // XOR inherits the top-most block's fill. let xorResult = try engine.block.combine( [xorA, xorB], booleanOperation: .xor, ) try engine.block.setName(xorResult, name: "XOR") ``` Use XOR for donut shapes, ring outlines, and any compound where you need the union of two shapes minus the area they share. ## Understanding Fill Inheritance The combined block inherits the prioritized block's **parent**, **sort order**, and appearance — including its **fill**, **stroke**, **opacity**, **blend mode**, **drop shadow**, **blur**, and **effects**. The prioritized block is top-most for Union and XOR, bottom-most for Difference and Intersection. The result's shape is computed by the boolean operation itself rather than copied from any single input. Operations apply pair-wise in visual stacking order: Union and XOR start from the top-most input block and walk down; Difference and Intersection start from the bottom-most input block and walk up. Reorder blocks with `engine.block.bringToFront(_:)` or `engine.block.sendToBack(_:)` before combining to control both which appearance survives and the order in which pairs are combined. ## Scope Requirements Combining blocks uses the same scope system as other block mutations: - `"lifecycle/duplicate"` must be enabled for every input block. `engine.block.isCombinable(_:)` checks this, along with the two-block and same-page requirements, before you call `combine(_:booleanOperation:)`. - `"lifecycle/destroy"` must be enabled when the input blocks should be replaced by the combined result. If scoped content blocks a combination workflow, inspect the relevant scope with `engine.block.isScopeEnabled(_:key:)` and update it with `engine.block.setScopeEnabled(_:key:enabled:)` only when your editing rules allow that change. ## Troubleshooting ### Combination Fails - Check the block array with `engine.block.isCombinable(_:)` before calling `combine(_:booleanOperation:)`. - Make sure every input is a graphic or text block. - Pass at least two compatible blocks on the same page. - Verify that the selected blocks still exist and have not already been consumed by a previous boolean operation. ### Wrong Fill on the Result - For Union and XOR, move the block whose fill should be inherited to the front. - For Difference and Intersection, send the block whose fill should be inherited to the back. - Re-run the combinability check after changing the selection or replacing any block. ### Original Blocks Remain If the combined result appears but the original blocks remain, the input blocks may not have `"lifecycle/destroy"` enabled. Check that scope with `engine.block.isScopeEnabled(_:key:)` and only enable it with `engine.block.setScopeEnabled(_:key:enabled:)` when your app's editing rules allow the originals to be removed. ### Unexpected Shape Result - Boolean operations use block order. Union and XOR start from the highest sort order; Difference and Intersection start from the lowest sort order. - Control visual stacking with `engine.block.bringToFront(_:)` or `engine.block.sendToBack(_:)` before combining. ## API Reference | Method | Purpose | | --- | --- | | `engine.block.isCombinable(_:)` | Check whether blocks can be combined | | `engine.block.combine(_:booleanOperation:)` | Perform a boolean operation on compatible blocks | | `engine.block.create(_:)` | Create a graphic or text block | | `engine.block.createShape(_:)` | Create a shape for a graphic block | | `engine.block.setShape(_:shape:)` | Assign a shape to a graphic block | | `engine.block.createFill(_:)` | Create a fill | | `engine.block.setFill(_:fill:)` | Assign a fill to a block | | `engine.block.setColor(_:property:color:)` | Set a color value on a fill | | `engine.block.setURL(_:property:value:)` | Set a URL property such as an image fill source | | `engine.block.setWidth(_:value:)` | Set the block width | | `engine.block.setHeight(_:value:)` | Set the block height | | `engine.block.setPositionX(_:value:)` | Set the block's x position | | `engine.block.setPositionY(_:value:)` | Set the block's y position | | `engine.block.appendChild(to:child:)` | Add a block to a parent | | `engine.block.forceLoadResources(_:)` | Load image fills and fonts needed by blocks | | `engine.block.setName(_:name:)` | Assign a name to a block | | `engine.block.bringToFront(_:)` | Move a block to the highest sort order | | `engine.block.sendToBack(_:)` | Move a block to the lowest sort order | | `engine.block.isScopeEnabled(_:key:)` | Check whether a scope is enabled | | `engine.block.setScopeEnabled(_:key:enabled:)` | Enable or disable a scope | ## Next Steps - [Create Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-shapes-64acc0/) — Draw the building-block shapes that boolean operations combine. - [Edit Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) — Modify shape properties before or after combining. - [Create Cutout](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-cutout-384be3/) — Generate cutout paths for die-cut prints and stickers. --- ## 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 Cutout" description: "Create cutouts from images or shapes by masking or removing specific areas." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-cutout-384be3/" --- > 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 Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-3d4e5f/) > [Create Cutout](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-cutout-384be3/) --- ```swift file=@cesdk_swift_examples/engine-guides-cutouts/Cutouts.swift reference-only import Foundation import IMGLYEngine @MainActor func cutouts(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 circle = try engine.block.createCutoutFromPath("M 0,25 a 25,25 0 1,1 50,0 a 25,25 0 1,1 -50,0 Z") try engine.block.appendChild(to: page, child: circle) try engine.block.setEnum(circle, property: "cutout/type", value: "Dashed") try engine.block.setFloat(circle, property: "cutout/offset", value: 3.0) try engine.block.setFloat(circle, property: "cutout/smoothing", value: 2.0) let square = try engine.block.createCutoutFromPath("M 0,0 H 50 V 50 H 0 Z") try engine.block.setFloat(square, property: "cutout/offset", value: 6.0) try engine.block.appendChild(to: page, child: square) let union = try engine.block.createCutoutFromOperation( containing: [circle, square], cutoutOperation: .union, ) try engine.block.appendChild(to: page, child: union) try engine.block.destroy(circle) try engine.block.destroy(square) engine.editor.setSpotColor(name: "CutContour", r: 0.0, g: 0.0, b: 1.0) engine.editor.setSpotColor(name: "PerfCutContour", r: 1.0, g: 0.5, b: 0.0) let graphic = try engine.block.create(.graphic) try engine.block.setShape(graphic, 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, g: 0, b: 0)) try engine.block.setFill(graphic, fill: fill) try engine.block.setWidth(graphic, value: 100) try engine.block.setHeight(graphic, value: 100) try engine.block.appendChild(to: page, child: graphic) let traced = try engine.block.createCutoutFromBlocks( ids: [graphic], vectorizeDistanceThreshold: 2, simplifyDistanceThreshold: 4, useExistingShapeInformation: true, ) try engine.block.appendChild(to: page, child: traced) } ``` Create cutout paths for cutting printers to produce die-cut stickers, iron-on decals, and custom-shaped prints programmatically. > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-cutouts) Cutouts define outline paths that cutting printers cut with a blade rather than print with ink. CE.SDK supports creating cutouts from SVG paths, generating them from block contours, and combining them with boolean operations. This guide covers creating cutouts programmatically from SVG paths and existing blocks, configuring cutout type, offset, and smoothing, combining cutouts with boolean operations, and customizing spot colors for printer compatibility. ## Understanding Cutouts Cutouts are special blocks of type `//ly.img.ubq/cutout` that contain SVG paths interpreted by cutting printers as cut lines. Printers recognize cutouts through specially named spot colors: `CutContour` for solid continuous cuts and `PerfCutContour` for dashed perforated cuts. The spot color RGB approximation affects on-screen rendering but not printer behavior. By default, both solid and dashed cutouts render as magenta — set their spot color RGB approximations via [Customizing Spot Colors](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-cutout-384be3/#customizing-spot-colors) to differentiate them visually on the canvas. > **Note:** Cutouts export to PDF format with spot color information preserved. Cutting > printers read the spot colors to identify cut paths. ## Setup the Scene Create a new scene with a page to host the cutouts. ```swift highlight-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) ``` ## Creating Cutouts from SVG Paths Create cutouts using `engine.block.createCutoutFromPath(_:)` with standard SVG path syntax. The path coordinates define the cutout dimensions. ```swift highlight-create-cutout-from-path let circle = try engine.block.createCutoutFromPath("M 0,25 a 25,25 0 1,1 50,0 a 25,25 0 1,1 -50,0 Z") try engine.block.appendChild(to: page, child: circle) ``` The method accepts standard SVG path commands: `M` (move), `L` (line), `H` (horizontal), `V` (vertical), `C` (cubic curve), `Q` (quadratic curve), `A` (arc), and `Z` (close path). Append the cutout to the page hierarchy so it participates in layout and export. ## Configuring Cutout Type Set the cutout type with `engine.block.setEnum(_:property:value:)` on `cutout/type`. The value is the string key `"Solid"` (default) for a continuous cut line or `"Dashed"` for a perforated cut line. ```swift highlight-configure-cutout-type try engine.block.setEnum(circle, property: "cutout/type", value: "Dashed") ``` `"Solid"` uses the `CutContour` spot color, while `"Dashed"` uses `PerfCutContour` for tear-away edges. ## Configuring Cutout Offset Adjust the distance between the cutout line and the source path with `engine.block.setFloat(_:property:value:)` on `cutout/offset`. The value is in the scene's design units. ```swift highlight-configure-cutout-offset try engine.block.setFloat(circle, property: "cutout/offset", value: 3.0) ``` Positive offset values expand the cutout outward from the path. Use offset to add bleed or margin around designs for cleaner cuts. ## Configuring Cutout Smoothing Round out sharp corners in the cutout path with `engine.block.setFloat(_:property:value:)` on `cutout/smoothing`. The value is a pixel threshold for corner rounding. ```swift highlight-configure-cutout-smoothing try engine.block.setFloat(circle, property: "cutout/smoothing", value: 2.0) ``` Smoothing is useful when generating cutouts from blocks with angular contours that would otherwise produce jagged cut lines. ## Creating Multiple Cutouts Create additional cutouts with independent properties to demonstrate combining them later. Each cutout carries its own type and offset. ```swift highlight-create-square-cutout let square = try engine.block.createCutoutFromPath("M 0,0 H 50 V 50 H 0 Z") try engine.block.setFloat(square, property: "cutout/offset", value: 6.0) try engine.block.appendChild(to: page, child: square) ``` ## Combining Cutouts with Boolean Operations Combine multiple cutouts into compound shapes with `engine.block.createCutoutFromOperation(containing:cutoutOperation:)`. The `CutoutOperation` enum exposes `.union`, `.difference`, `.intersection`, and `.xor`. ```swift highlight-combine-cutouts let union = try engine.block.createCutoutFromOperation( containing: [circle, square], cutoutOperation: .union, ) try engine.block.appendChild(to: page, child: union) try engine.block.destroy(circle) try engine.block.destroy(square) ``` The combined cutout inherits the type from the first cutout in the array and has an offset of 0. Destroy the original cutouts after combining to avoid duplicate cuts during printing. > **Note:** When using `.difference`, the first cutout is the base that the others > subtract from. For other operations, the order affects which cutout's type is > inherited. ## Customizing Spot Colors Modify the spot color RGB approximation with `engine.editor.setSpotColor(name:r:g:b:)` to change how cutouts render without affecting printer behavior. An overload accepting CMYK components is also available for print workflows. ```swift highlight-customize-spot-color engine.editor.setSpotColor(name: "CutContour", r: 0.0, g: 0.0, b: 1.0) engine.editor.setSpotColor(name: "PerfCutContour", r: 1.0, g: 0.5, b: 0.0) ``` Spot color names (`CutContour`, `PerfCutContour`) are what printers recognize. Adjust the names with `engine.editor.setSpotColor` if your printer uses different conventions. ## Creating Cutouts from Blocks Generate cutouts automatically from existing block contours with `engine.block.createCutoutFromBlocks(ids:...)`. The method vectorizes block appearances or reuses existing vector paths. ```swift highlight-create-cutout-from-blocks let graphic = try engine.block.create(.graphic) try engine.block.setShape(graphic, 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, g: 0, b: 0)) try engine.block.setFill(graphic, fill: fill) try engine.block.setWidth(graphic, value: 100) try engine.block.setHeight(graphic, value: 100) try engine.block.appendChild(to: page, child: graphic) let traced = try engine.block.createCutoutFromBlocks( ids: [graphic], vectorizeDistanceThreshold: 2, simplifyDistanceThreshold: 4, useExistingShapeInformation: true, ) try engine.block.appendChild(to: page, child: traced) ``` | Parameter | Purpose | |---|---| | `vectorizeDistanceThreshold` | Maximum number of pixels by which the cutout path can deviate from the original contour during vectorization. | | `simplifyDistanceThreshold` | Maximum number of pixels by which the simplified path can deviate from the vectorized contour. Pass `0` to disable simplification. | | `useExistingShapeInformation` | When `true`, reuses the existing vector paths of the provided blocks. When `false`, generates new shape information. | ## Troubleshooting ### Cutout Not Visible Cutouts render using spot color RGB approximations. Verify the cutout is appended to the page hierarchy with `engine.block.appendChild(to:child:)` and positioned within the visible canvas area. ### Printer Not Cutting Check that spot color names match your printer's requirements. Some printers need specific names like `CutContour` or `Thru-cut`. Consult your printer documentation. ### Combined Cutout Has Wrong Type Combined cutouts inherit the type from the first cutout in the array. Reorder the array passed to `createCutoutFromOperation(containing:cutoutOperation:)` or set the type explicitly after combination. ### Cutout Path Too Complex Use `createCutoutFromBlocks(ids:...)` with a higher `simplifyDistanceThreshold` to reduce the number of points in the path. Complex paths may cause performance issues or printer errors. ## API Reference | Method | Category | Purpose | |---|---|---| | `engine.block.createCutoutFromPath(_:)` | Cutout | Create cutout from SVG path string | | `engine.block.createCutoutFromBlocks(ids:...)` | Cutout | Create cutout from block contours | | `engine.block.createCutoutFromOperation(containing:cutoutOperation:)` | Cutout | Combine cutouts with boolean operation | | `engine.block.setEnum(_:property:value:)` on `cutout/type` | Property | Set cutout type (`"Solid"` / `"Dashed"`) | | `engine.block.setFloat(_:property:value:)` on `cutout/offset` | Property | Set cutout offset distance | | `engine.block.setFloat(_:property:value:)` on `cutout/smoothing` | Property | Set corner smoothing threshold | | `engine.block.appendChild(to:child:)` | Hierarchy | Add cutout to scene | | `engine.block.destroy(_:)` | Lifecycle | Remove cutout from scene | | `engine.editor.setSpotColor(name:r:g:b:)` | Editor | Customize spot color rendering (RGB) | | `engine.editor.setSpotColor(name:c:m:y:k:)` | Editor | Customize spot color rendering (CMYK) | ## Next Steps - [Combine Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/combine-2a9e26/) — Boolean operations on graphic blocks - [Create Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-shapes-64acc0/) — Create geometric shapes programmatically - [Export for Printing](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/for-printing-bca896/) — Export print-ready PDFs with spot 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: "Create Shapes" description: "Create and configure geometric shapes programmatically using the Engine API in CE.SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-shapes-64acc0/" --- > 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 Shapes](https://img.ly/docs/cesdk/mac-catalyst/shapes-9f1b2c/) > [Create Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-shapes-64acc0/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-shapes/CreateShapes.swift reference-only import IMGLYEngine @MainActor func createShapes(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 let probeBlock = try engine.block.create(.graphic) print("Graphic supports shape:", try engine.block.supportsShape(probeBlock)) // true let text = try engine.block.create(.text) print("Text supports shape:", try engine.block.supportsShape(text)) // false try engine.block.destroy(probeBlock) try engine.block.destroy(text) let rectangleBlock = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(rectangleBlock, shape: rectShape) let colorFill = try engine.block.createFill(.color) try engine.block.setColor( colorFill, property: "fill/color/value", color: .rgba(r: 0.85, g: 0.25, b: 0.25), ) try engine.block.setFill(rectangleBlock, fill: colorFill) try engine.block.setWidth(rectangleBlock, value: 320) try engine.block.setHeight(rectangleBlock, value: 220) try engine.block.setPositionX(rectangleBlock, value: 40) try engine.block.setPositionY(rectangleBlock, value: 40) try engine.block.appendChild(to: page, child: rectangleBlock) let ellipseBlock = try engine.block.create(.graphic) let ellipseShape = try engine.block.createShape(.ellipse) try engine.block.setShape(ellipseBlock, shape: ellipseShape) let gradientFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( gradientFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.2, g: 0.6, b: 0.95), stop: 0), GradientColorStop(color: .rgba(r: 0.1, g: 0.2, b: 0.6), stop: 1), ], ) try engine.block.setFill(ellipseBlock, fill: gradientFill) try engine.block.setWidth(ellipseBlock, value: 320) try engine.block.setHeight(ellipseBlock, value: 220) try engine.block.setPositionX(ellipseBlock, value: 440) try engine.block.setPositionY(ellipseBlock, value: 40) try engine.block.appendChild(to: page, child: ellipseBlock) let starBlock = try engine.block.create(.graphic) let starShape = try engine.block.createShape(.star) try engine.block.setShape(starBlock, shape: starShape) try engine.block.setInt(starShape, property: "shape/star/points", value: 6) try engine.block.setFloat(starShape, property: "shape/star/innerDiameter", value: 0.5) let starFill = try engine.block.createFill(.color) try engine.block.setColor( starFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.75, b: 0.2), ) try engine.block.setFill(starBlock, fill: starFill) try engine.block.setWidth(starBlock, value: 220) try engine.block.setHeight(starBlock, value: 220) try engine.block.setPositionX(starBlock, value: 40) try engine.block.setPositionY(starBlock, value: 320) try engine.block.appendChild(to: page, child: starBlock) let polygonBlock = try engine.block.create(.graphic) let polygonShape = try engine.block.createShape(.polygon) try engine.block.setShape(polygonBlock, shape: polygonShape) try engine.block.setInt(polygonShape, property: "shape/polygon/sides", value: 6) let polygonFill = try engine.block.createFill(.color) try engine.block.setColor( polygonFill, property: "fill/color/value", color: .rgba(r: 0.3, g: 0.75, b: 0.4), ) try engine.block.setFill(polygonBlock, fill: polygonFill) try engine.block.setWidth(polygonBlock, value: 220) try engine.block.setHeight(polygonBlock, value: 220) try engine.block.setPositionX(polygonBlock, value: 290) try engine.block.setPositionY(polygonBlock, value: 320) try engine.block.appendChild(to: page, child: polygonBlock) let lineBlock = try engine.block.create(.graphic) let lineShape = try engine.block.createShape(.line) try engine.block.setShape(lineBlock, shape: lineShape) let lineFill = try engine.block.createFill(.color) try engine.block.setColor( lineFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.2, b: 0.2), ) try engine.block.setFill(lineBlock, fill: lineFill) try engine.block.setWidth(lineBlock, value: 220) try engine.block.setHeight(lineBlock, value: 8) try engine.block.setPositionX(lineBlock, value: 540) try engine.block.setPositionY(lineBlock, value: 360) try engine.block.appendChild(to: page, child: lineBlock) let arrowBlock = try engine.block.create(.graphic) let arrowShape = try engine.block.createShape(.vectorPath) try engine.block.setString( arrowShape, property: "shape/vector_path/path", value: "M 0,40 L 60,40 L 60,20 L 100,50 L 60,80 L 60,60 L 0,60 Z", ) try engine.block.setShape(arrowBlock, shape: arrowShape) let arrowFill = try engine.block.createFill(.color) try engine.block.setColor( arrowFill, property: "fill/color/value", color: .rgba(r: 0.55, g: 0.3, b: 0.75), ) try engine.block.setFill(arrowBlock, fill: arrowFill) try engine.block.setWidth(arrowBlock, value: 220) try engine.block.setHeight(arrowBlock, value: 120) try engine.block.setPositionX(arrowBlock, value: 540) try engine.block.setPositionY(arrowBlock, value: 420) try engine.block.appendChild(to: page, child: arrowBlock) try await engine.captureGuide(page, label: "hero") let starProperties = try engine.block.findAllProperties(starShape) print("Star properties:", starProperties) try engine.block.setFloat(rectShape, property: "shape/rect/cornerRadiusTL", value: 20) try engine.block.setFloat(rectShape, property: "shape/rect/cornerRadiusTR", value: 20) try engine.block.setFloat(rectShape, property: "shape/rect/cornerRadiusBL", value: 20) try engine.block.setFloat(rectShape, property: "shape/rect/cornerRadiusBR", value: 20) let imageBlock = try engine.block.create(.graphic) let imageRect = try engine.block.createShape(.rect) try engine.block.setShape(imageBlock, shape: imageRect) 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.destroy(imageBlock) let currentShape = try engine.block.getShape(rectangleBlock) let currentShapeType = try engine.block.getType(currentShape) print("Current shape type:", currentShapeType) let swapBlock = try engine.block.create(.graphic) let oldShape = try engine.block.createShape(.rect) try engine.block.setShape(swapBlock, shape: oldShape) let newShape = try engine.block.createShape(.ellipse) try engine.block.destroy(try engine.block.getShape(swapBlock)) try engine.block.setShape(swapBlock, shape: newShape) try engine.block.destroy(swapBlock) let independentBlock = try engine.block.create(.graphic) let initialShape = try engine.block.createShape(.star) try engine.block.setShape(independentBlock, shape: initialShape) let initialFill = try engine.block.createFill(.color) try engine.block.setColor( initialFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.0, b: 0.0), ) try engine.block.setFill(independentBlock, fill: initialFill) // Swap the shape, keep the same fill. let replacementShape = try engine.block.createShape(.rect) try engine.block.destroy(try engine.block.getShape(independentBlock)) try engine.block.setShape(independentBlock, shape: replacementShape) // Swap the fill, keep the rectangular shape. let replacementFill = try engine.block.createFill(.color) try engine.block.setColor( replacementFill, property: "fill/color/value", color: .rgba(r: 0.0, g: 0.0, b: 1.0), ) try engine.block.destroy(try engine.block.getFill(independentBlock)) try engine.block.setFill(independentBlock, fill: replacementFill) try engine.block.destroy(independentBlock) } ``` Create and configure geometric shapes programmatically using the Engine API—rectangles, ellipses, stars, polygons, lines, and custom vector paths combined with fills. ![Six shape types — rectangle, ellipse, star, polygon, line, and vector path — rendered side by side with solid colors and a linear gradient](./assets/swift-based.hero.webp) > **Reading time:** 15 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-create-shapes) ## Understanding Shapes and Graphic Blocks ### What Are Shapes? Shapes in CE.SDK are geometric definitions — rectangles, ellipses, stars, and other forms — that exist as independent objects until attached to graphic blocks. Create shapes with `createShape(_:)` using a `ShapeType` like `.rect` or `.ellipse`. Shapes define the geometry and remain invisible until combined with fills. Shapes and fills are independent. Swap a rectangle for a star while keeping the same fill, or replace a color fill with a gradient while keeping the same shape. ### The Graphic Block System Graphic blocks are containers that bring shapes and fills together. A new graphic block starts empty — no shape, no fill, and therefore invisible. Apply both a shape and a fill to render it on the canvas. A graphic block holds: - **Shape**: The geometric form (rectangle, ellipse, star, polygon, line, or vector path) - **Fill**: The color, gradient, image, or video content that makes the shape visible - **Effects**: Optional filters, blur, or shadows applied to the filled shape - **Transform**: Position, rotation, and scale properties ### Available Shape Types CE.SDK provides six built-in shape types, exposed through the `ShapeType` enum: - **Rectangle** (`.rect`): Basic rectangular shapes with optional rounded corners - **Ellipse** (`.ellipse`): Circular and oval shapes - **Star** (`.star`): Star shapes with configurable points and inner diameter - **Polygon** (`.polygon`): Regular polygons with a configurable number of sides - **Line** (`.line`): Straight lines spanning the block's dimensions - **Vector Path** (`.vectorPath`): Custom shapes built from SVG path data ## Checking Shape Support Verify that a block type supports shapes before applying them. Not all block types can hold a shape — graphic blocks support shapes, while text blocks, scenes, and pages do not. ```swift highlight-createShapes-checkSupport let probeBlock = try engine.block.create(.graphic) print("Graphic supports shape:", try engine.block.supportsShape(probeBlock)) // true let text = try engine.block.create(.text) print("Text supports shape:", try engine.block.supportsShape(text)) // false ``` `supportsShape(_:)` returns `true` for graphic blocks and `false` for text blocks. Always check before working with dynamic or unknown block types. ## Creating Basic Shapes ### Creating a Rectangle Create a graphic block, apply a rectangle shape, and add a color fill to make it visible: ```swift highlight-createShapes-createRectangle let rectangleBlock = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(rectangleBlock, shape: rectShape) let colorFill = try engine.block.createFill(.color) try engine.block.setColor( colorFill, property: "fill/color/value", color: .rgba(r: 0.85, g: 0.25, b: 0.25), ) try engine.block.setFill(rectangleBlock, fill: colorFill) try engine.block.setWidth(rectangleBlock, value: 320) try engine.block.setHeight(rectangleBlock, value: 220) try engine.block.setPositionX(rectangleBlock, value: 40) try engine.block.setPositionY(rectangleBlock, value: 40) try engine.block.appendChild(to: page, child: rectangleBlock) ``` The shape defines the geometry; the fill provides the visual content. Both must be set before the block renders. ### Creating an Ellipse with a Gradient Fill Create an ellipse the same way — only the shape type changes. Pair it with a linear gradient fill for a smooth color transition: ```swift highlight-createShapes-createEllipse let ellipseBlock = try engine.block.create(.graphic) let ellipseShape = try engine.block.createShape(.ellipse) try engine.block.setShape(ellipseBlock, shape: ellipseShape) let gradientFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( gradientFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.2, g: 0.6, b: 0.95), stop: 0), GradientColorStop(color: .rgba(r: 0.1, g: 0.2, b: 0.6), stop: 1), ], ) try engine.block.setFill(ellipseBlock, fill: gradientFill) ``` The gradient stops are defined with `GradientColorStop(color:stop:)` values where `stop` runs from `0` to `1`. ### Creating a Star A star shape with a configurable number of points: ```swift highlight-createShapes-createStar let starBlock = try engine.block.create(.graphic) let starShape = try engine.block.createShape(.star) try engine.block.setShape(starBlock, shape: starShape) try engine.block.setInt(starShape, property: "shape/star/points", value: 6) try engine.block.setFloat(starShape, property: "shape/star/innerDiameter", value: 0.5) let starFill = try engine.block.createFill(.color) try engine.block.setColor( starFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.75, b: 0.2), ) try engine.block.setFill(starBlock, fill: starFill) ``` ### Creating a Polygon A regular polygon with a configurable number of sides: ```swift highlight-createShapes-createPolygon let polygonBlock = try engine.block.create(.graphic) let polygonShape = try engine.block.createShape(.polygon) try engine.block.setShape(polygonBlock, shape: polygonShape) try engine.block.setInt(polygonShape, property: "shape/polygon/sides", value: 6) let polygonFill = try engine.block.createFill(.color) try engine.block.setColor( polygonFill, property: "fill/color/value", color: .rgba(r: 0.3, g: 0.75, b: 0.4), ) try engine.block.setFill(polygonBlock, fill: polygonFill) ``` ### Creating a Line Lines span their block's dimensions. The block's width and height control the line's length and stroke thickness: ```swift highlight-createShapes-createLine let lineBlock = try engine.block.create(.graphic) let lineShape = try engine.block.createShape(.line) try engine.block.setShape(lineBlock, shape: lineShape) let lineFill = try engine.block.createFill(.color) try engine.block.setColor( lineFill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.2, b: 0.2), ) try engine.block.setFill(lineBlock, fill: lineFill) ``` ### Creating a Vector Path A custom shape built from SVG path data: ```swift highlight-createShapes-createVectorPath let arrowBlock = try engine.block.create(.graphic) let arrowShape = try engine.block.createShape(.vectorPath) try engine.block.setString( arrowShape, property: "shape/vector_path/path", value: "M 0,40 L 60,40 L 60,20 L 100,50 L 60,80 L 60,60 L 0,60 Z", ) try engine.block.setShape(arrowBlock, shape: arrowShape) let arrowFill = try engine.block.createFill(.color) try engine.block.setColor( arrowFill, property: "fill/color/value", color: .rgba(r: 0.55, g: 0.3, b: 0.75), ) try engine.block.setFill(arrowBlock, fill: arrowFill) ``` ## Configuring Shape Properties ### Discovering Properties Each shape type exposes its own set of properties. Use `findAllProperties(_:)` to list everything you can set on a given shape: ```swift highlight-createShapes-discoverProperties let starProperties = try engine.block.findAllProperties(starShape) print("Star properties:", starProperties) ``` The call returns a list like `["includedInExport", "name", "shape/star/cornerRadius", "shape/star/innerDiameter", "shape/star/points", "type", "uuid"]`, which you can then pass to the matching setter — `setInt` for integers, `setFloat` for floats, `setString` for strings. ### Rectangle: Corner Radius Rectangles expose four independent corner radius properties — `cornerRadiusTL`, `cornerRadiusTR`, `cornerRadiusBL`, and `cornerRadiusBR` — measured in the scene's design units: ```swift highlight-createShapes-cornerRadius try engine.block.setFloat(rectShape, property: "shape/rect/cornerRadiusTL", value: 20) try engine.block.setFloat(rectShape, property: "shape/rect/cornerRadiusTR", value: 20) try engine.block.setFloat(rectShape, property: "shape/rect/cornerRadiusBL", value: 20) try engine.block.setFloat(rectShape, property: "shape/rect/cornerRadiusBR", value: 20) ``` Set all four to the same value for uniform rounded corners. Mix values to create asymmetric shapes like ticket stubs or chat bubbles. ### Star: Points and Inner Diameter Star shapes are configured through two properties: ```swift highlight-createShapes-createStar let starBlock = try engine.block.create(.graphic) let starShape = try engine.block.createShape(.star) try engine.block.setShape(starBlock, shape: starShape) try engine.block.setInt(starShape, property: "shape/star/points", value: 6) try engine.block.setFloat(starShape, property: "shape/star/innerDiameter", value: 0.5) let starFill = try engine.block.createFill(.color) try engine.block.setColor( starFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.75, b: 0.2), ) try engine.block.setFill(starBlock, fill: starFill) ``` `shape/star/points` (integer, minimum 3) sets the number of star tips. `shape/star/innerDiameter` (0.0 to 1.0) controls the ratio between the inner and outer radius — smaller values produce sharper, more pronounced points. ### Polygon: Number of Sides Polygons render as regular shapes with all sides equal. `shape/polygon/sides` (integer, minimum 3) selects between a triangle, square, pentagon, hexagon, and beyond: ```swift highlight-createShapes-createPolygon let polygonBlock = try engine.block.create(.graphic) let polygonShape = try engine.block.createShape(.polygon) try engine.block.setShape(polygonBlock, shape: polygonShape) try engine.block.setInt(polygonShape, property: "shape/polygon/sides", value: 6) let polygonFill = try engine.block.createFill(.color) try engine.block.setColor( polygonFill, property: "fill/color/value", color: .rgba(r: 0.3, g: 0.75, b: 0.4), ) try engine.block.setFill(polygonBlock, fill: polygonFill) ``` ### Vector Paths: Custom SVG Paths Vector paths accept custom SVG path data through the `shape/vector_path/path` string property: ```swift highlight-createShapes-createVectorPath let arrowBlock = try engine.block.create(.graphic) let arrowShape = try engine.block.createShape(.vectorPath) try engine.block.setString( arrowShape, property: "shape/vector_path/path", value: "M 0,40 L 60,40 L 60,20 L 100,50 L 60,80 L 60,60 L 0,60 Z", ) try engine.block.setShape(arrowBlock, shape: arrowShape) let arrowFill = try engine.block.createFill(.color) try engine.block.setColor( arrowFill, property: "fill/color/value", color: .rgba(r: 0.55, g: 0.3, b: 0.75), ) try engine.block.setFill(arrowBlock, fill: arrowFill) ``` Use standard SVG path commands: - `M x,y` — move to absolute coordinates - `L x,y` — line to absolute coordinates - `C x1,y1 x2,y2 x,y` — cubic Bezier curve - `Q x1,y1 x,y` — quadratic Bezier curve - `A rx,ry rotation large-arc sweep x,y` — arc - `Z` — close path Vector paths support a single continuous path. Coordinates are interpreted in the path's own bounding box and scaled to the block's dimensions. For complex multi-path graphics, use an image fill backed by an SVG file instead. ## Combining Shapes with Fills ### Why Fills Matter Shapes define geometry but remain invisible without fills. Fills supply the visual content — solid colors, gradients, images, or video — that the engine renders inside the shape's outline. ### Applying an Image Fill Apply an image fill the same way as a color fill — only the fill type and properties change. Set the source URI on the fill before attaching it to the block: ```swift highlight-createShapes-imageFill let imageBlock = try engine.block.create(.graphic) let imageRect = try engine.block.createShape(.rect) try engine.block.setShape(imageBlock, shape: imageRect) 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) ``` For comprehensive fill system documentation, see [Fills Overview](https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/). ### Shape and Fill Independence Shapes and fills operate independently. Replace a shape while keeping the fill, or change the fill while keeping the shape: ```swift highlight-createShapes-independence let independentBlock = try engine.block.create(.graphic) let initialShape = try engine.block.createShape(.star) try engine.block.setShape(independentBlock, shape: initialShape) let initialFill = try engine.block.createFill(.color) try engine.block.setColor( initialFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.0, b: 0.0), ) try engine.block.setFill(independentBlock, fill: initialFill) // Swap the shape, keep the same fill. let replacementShape = try engine.block.createShape(.rect) try engine.block.destroy(try engine.block.getShape(independentBlock)) try engine.block.setShape(independentBlock, shape: replacementShape) // Swap the fill, keep the rectangular shape. let replacementFill = try engine.block.createFill(.color) try engine.block.setColor( replacementFill, property: "fill/color/value", color: .rgba(r: 0.0, g: 0.0, b: 1.0), ) try engine.block.destroy(try engine.block.getFill(independentBlock)) try engine.block.setFill(independentBlock, fill: replacementFill) ``` Destroying the old shape or fill before replacing it prevents memory leaks. The graphic block adopts the new value. ## Managing Shapes ### Retrieving the Current Shape Read the shape attached to any graphic block with `getShape(_:)`, then identify its type with `getType(_:)`: ```swift highlight-createShapes-retrieveShape let currentShape = try engine.block.getShape(rectangleBlock) let currentShapeType = try engine.block.getType(currentShape) print("Current shape type:", currentShapeType) ``` Use this pattern to inspect and modify existing shapes. ### Replacing a Shape When swapping shapes, destroy the previous one to release its memory. Blocks automatically destroy their attached shape when the block itself is destroyed, but a manually replaced shape must be cleaned up: ```swift highlight-createShapes-replaceShape let swapBlock = try engine.block.create(.graphic) let oldShape = try engine.block.createShape(.rect) try engine.block.setShape(swapBlock, shape: oldShape) let newShape = try engine.block.createShape(.ellipse) try engine.block.destroy(try engine.block.getShape(swapBlock)) try engine.block.setShape(swapBlock, shape: newShape) ``` ## Troubleshooting ### Shape Not Visible If the shape doesn't appear, confirm three conditions: - **A fill is attached.** Read `getFill(_:)`; if it returns an invalid `DesignBlockID`, attach a fill with `createFill(.color)` and `setFill(_:fill:)`. - **Width and height are non-zero.** A block with zero dimensions renders nothing. - **The block is in the scene hierarchy.** A graphic block that hasn't been appended to a page (or other parent) does not render. ### Cannot Apply Shape Calling `setShape(_:shape:)` on a block that doesn't support shapes throws. Always gate the call with `supportsShape(_:)`. Use a graphic block when the target block type doesn't allow shapes. ### Shape Properties Not Changing Match the setter method to the property's value type — `setInt` for integers, `setFloat` for floats, and `setString` for strings. List available properties on the shape with `findAllProperties(_:)` and use exact property paths from the result. `shape/star/points` and `shape/polygon/sides` should be 3 or higher — the setter accepts smaller values without error, but a star or polygon needs at least 3 tips/sides to render as a recognizable shape. ## API Reference | Method | Description | Returns | | ----------------------------------------------- | ---------------------------------------------------- | --------------- | | `block.create(.graphic)` | Create a new graphic block that can hold a shape | `DesignBlockID` | | `block.createShape(_:)` | Create a new shape of the given `ShapeType` | `DesignBlockID` | | `block.supportsShape(_:)` | Check whether a block type supports shapes | `Bool` | | `block.setShape(_:shape:)` | Attach a shape to a graphic block | `Void` | | `block.getShape(_:)` | Get the shape currently attached to a graphic block | `DesignBlockID` | | `block.getType(_:)` | Get the type identifier string for any block | `String` | | `block.findAllProperties(_:)` | List every property available on the given block | `[String]` | | `block.setInt(_:property:value:)` | Set an integer property (`points`, `sides`) | `Void` | | `block.setFloat(_:property:value:)` | Set a float property (corner radius, inner diameter) | `Void` | | `block.setString(_:property:value:)` | Set a string property (vector path data) | `Void` | | `block.createFill(_:)` | Create a fill of the given `FillType` | `DesignBlockID` | | `block.setFill(_:fill:)` | Attach a fill so the shape becomes visible | `Void` | | `block.setColor(_:property:color:)` | Set a color property to a `Color` value | `Void` | | `block.setGradientColorStops(_:property:colors:)` | Set gradient stops for a gradient fill | `Void` | | `block.destroy(_:)` | Destroy a shape, fill, or block and free its memory | `Void` | **Available shape types** (`ShapeType`): - `.rect` — Rectangle - `.ellipse` — Ellipse / circle - `.star` — Star - `.polygon` — Polygon - `.line` — Line - `.vectorPath` — Custom vector path ## Next Steps - [Edit Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) — Modify shape properties and transforms after creation. - [Combine Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/combine-2a9e26/) — Build complex shapes with boolean operations. - [Color Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/color-7129cd/) — Apply solid colors with RGB, CMYK, and Spot Colors. - [Gradient Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/gradient-0ff079/) — Create linear, radial, and conical color transitions. - [Image Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/image-e9cb5c/) — Use photos and raster content inside shapes. - [Fills Overview](https://img.ly/docs/cesdk/mac-catalyst/fills/overview-3895ee/) — Tour the full fill 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: "Create Stickers" description: "Create stickers in CE.SDK using image fills for icons, logos, emoji, and multi-color graphics" platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-stickers-cc46e5/" --- > 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 Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-3d4e5f/) > [Create Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-stickers-cc46e5/) --- ```swift file=@cesdk_swift_examples/engine-guides-create-stickers/CreateStickers.swift reference-only import Foundation import IMGLYEngine @MainActor func createStickers(engine: Engine) async throws { // A 450x250 page hosts the sticker. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 450) try engine.block.setHeight(page, value: 250) try engine.block.appendChild(to: scene, child: page) // A sticker is a graphic block with a rect shape and an image fill. let sticker = try engine.block.create(.graphic) let stickerShape = try engine.block.createShape(.rect) try engine.block.setShape(sticker, shape: stickerShape) let stickerFill = try engine.block.createFill(.image) try engine.block.setString( stickerFill, property: "fill/image/imageFileURI", value: "https://cdn.img.ly/packages/imgly/cesdk-swift/1.76.0" + "/assets/ly.img.sticker/images/emoticons/imgly_sticker_emoticons_grin.svg", ) try engine.block.setFill(sticker, fill: stickerFill) // Preserve the source artwork's aspect ratio inside the block bounds. if try engine.block.supportsContentFillMode(sticker) { try engine.block.setContentFillMode(sticker, mode: .contain) } // Tag the block as a sticker so the editor categorizes it correctly and // exposes the sticker-specific inspector bar. try engine.block.setKind(sticker, kind: "sticker") try engine.block.setWidth(sticker, value: 150) try engine.block.setHeight(sticker, value: 150) try engine.block.setPositionX(sticker, value: 60) try engine.block.setPositionY(sticker, value: 50) try engine.block.appendChild(to: page, child: sticker) // Demo scaffolding: add a second sticker so the hero export shows the // multi-sticker layout the prose describes. The recipe is identical — only // the source URL and position differ. let secondSticker = try engine.block.create(.graphic) try engine.block.setShape(secondSticker, shape: try engine.block.createShape(.rect)) let secondFill = try engine.block.createFill(.image) try engine.block.setString( secondFill, property: "fill/image/imageFileURI", value: "https://cdn.img.ly/packages/imgly/cesdk-swift/1.76.0" + "/assets/ly.img.sticker/images/emoticons/imgly_sticker_emoticons_blush.svg", ) try engine.block.setFill(secondSticker, fill: secondFill) if try engine.block.supportsContentFillMode(secondSticker) { try engine.block.setContentFillMode(secondSticker, mode: .contain) } try engine.block.setKind(secondSticker, kind: "sticker") try engine.block.setWidth(secondSticker, value: 150) try engine.block.setHeight(secondSticker, value: 150) try engine.block.setPositionX(secondSticker, value: 240) try engine.block.setPositionY(secondSticker, value: 50) try engine.block.appendChild(to: page, child: secondSticker) try await engine.captureGuide(page, label: "hero") } ``` Create stickers from images for use in your designs, perfect for adding icons, logos, emoji, and detailed multi-color graphics that preserve their original appearance. ![Two emoticon stickers placed side by side on a page, preserving the colors of the source SVG artwork.](./assets/swift-based.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-create-stickers) Stickers are graphic blocks with image fills that cannot be recolored. They work well for icons, brand logos, emoji, and complex multi-color graphics. Unlike shapes (which use solid or gradient fills and can be recolored), stickers preserve the original colors and details of the source image. ## Creating Stickers from Images Build a sticker by composing the primitives that make a graphic block visible: a rect shape, an image fill, and the `"sticker"` kind tag. The image fill reads its source from the `fill/image/imageFileURI` property, and the `.contain` content fill mode preserves the source artwork's aspect ratio inside the block bounds. Repeat the same recipe for each additional sticker — each block carries its own image fill, so colors stay independent. ```swift highlight-createStickers-manualConstruction // A 450x250 page hosts the sticker. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 450) try engine.block.setHeight(page, value: 250) try engine.block.appendChild(to: scene, child: page) // A sticker is a graphic block with a rect shape and an image fill. let sticker = try engine.block.create(.graphic) let stickerShape = try engine.block.createShape(.rect) try engine.block.setShape(sticker, shape: stickerShape) let stickerFill = try engine.block.createFill(.image) try engine.block.setString( stickerFill, property: "fill/image/imageFileURI", value: "https://cdn.img.ly/packages/imgly/cesdk-swift/1.76.0" + "/assets/ly.img.sticker/images/emoticons/imgly_sticker_emoticons_grin.svg", ) try engine.block.setFill(sticker, fill: stickerFill) // Preserve the source artwork's aspect ratio inside the block bounds. if try engine.block.supportsContentFillMode(sticker) { try engine.block.setContentFillMode(sticker, mode: .contain) } // Tag the block as a sticker so the editor categorizes it correctly and // exposes the sticker-specific inspector bar. try engine.block.setKind(sticker, kind: "sticker") try engine.block.setWidth(sticker, value: 150) try engine.block.setHeight(sticker, value: 150) try engine.block.setPositionX(sticker, value: 60) try engine.block.setPositionY(sticker, value: 50) try engine.block.appendChild(to: page, child: sticker) ``` `setKind(_:kind:)` records a semantic tag the editor reads back to choose the right inspector controls; the engine itself ignores the value. Without the tag the block still renders, but the editor treats it as a regular image and may offer recolor options that do not apply to multi-color artwork. ## Sticker vs Shape Decision Choose between stickers and shapes based on your requirements: | Requirement | Use Stickers | Use Shapes | | --- | --- | --- | | Multi-color graphics | ✓ Yes | ✗ No (single fill) | | Recolorable | ✗ No | ✓ Yes | | Preserve original artwork | ✓ Yes | ✗ N/A | | Boolean operations | ✗ No | ✓ Yes | | Complex paths/gradients | ✓ Yes | ✗ Limited | | Icons, logos, emoji | ✓ Preferred | - | ## Troubleshooting ### Sticker Not Appearing Verify the image URL returns a valid image. Check that the sticker is appended to the current page. Ensure dimensions are non-zero. Confirm the image format is supported (SVG, PNG, JPG). ### Manually Created Sticker Is Blank Graphic blocks need both a shape and a fill to render. Call `engine.block.setShape(_:shape:)` with a rect shape before setting the image fill — without the shape the engine has nothing to draw, even when the fill is correctly configured. ### Sticker Appears Blurry For raster stickers, ensure the source image resolution matches or exceeds the displayed size. Use SVG stickers for scalable graphics that remain sharp at any size. ### Sticker Appears Cropped The block may render with the image cropped to its bounds. Switch to `.contain` so the entire image displays without cropping. Always check support first with `supportsContentFillMode(_:)`. ### Sticker Cannot Be Recolored This is expected behavior — stickers preserve the original colors of their source image. For recolorable graphics, create shapes with vector paths and a color or gradient fill instead. ### Wrong Editor Behavior Confirm `engine.block.setKind(_:kind: "sticker")` was called. The kind tag controls which editor surfaces the block is offered to and which inspector actions are exposed for it. ## API Reference Quick reference for the sticker creation methods used in this guide: | Method | Category | Purpose | | --- | --- | --- | | `engine.block.create(.graphic)` | Creation | Create a graphic block | | `engine.block.createShape(.rect)` | Shapes | Create a rect shape (required for visibility) | | `engine.block.setShape(_:shape:)` | Shapes | Apply a shape to a graphic block | | `engine.block.createFill(.image)` | Fills | Create an image fill | | `engine.block.setFill(_:fill:)` | Fills | Apply a fill to a graphic block | | `engine.block.setString(_:property:value:)` | Fills | Set `fill/image/imageFileURI` on the fill | | `engine.block.supportsContentFillMode(_:)` | Content | Check whether the block supports a content fill mode | | `engine.block.setContentFillMode(_:mode:)` | Content | Switch the content fill mode to `.contain` | | `engine.block.setKind(_:kind:)` | Configuration | Tag the block as a sticker | | `engine.block.setPositionX/Y(_:value:)` | Transform | Set the block's position | | `engine.block.setWidth/Height(_:value:)` | Transform | Set the block's dimensions | | `engine.block.appendChild(to:child:)` | Hierarchy | Add the block to a 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: "Edit Shapes" description: "Edit graphic block shapes programmatically — replace geometry, modify shape-specific properties, change fills and strokes, transform, combine, and group." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-shapes-d67cfb/" --- > 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 Shapes](https://img.ly/docs/cesdk/mac-catalyst/shapes-9f1b2c/) > [Edit Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) --- ```swift file=@cesdk_swift_examples/engine-guides-edit-shapes/EditShapes.swift reference-only import Foundation import IMGLYEngine @MainActor func editShapes(engine: Engine) async throws { // Demo scaffolding: a scene with a single page that holds every example // block in this guide. Each section creates one or more graphic blocks and // places them at fixed positions so the final hero capture shows the full // gallery. 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) // Start from a graphic block with a rectangle shape and a solid color fill. let demoBlock = try engine.block.create(.graphic) try engine.block.setShape(demoBlock, shape: engine.block.createShape(.rect)) let demoFill = try engine.block.createFill(.color) try engine.block.setColor( demoFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.85, b: 0.30, a: 1.0), ) try engine.block.setFill(demoBlock, fill: demoFill) try engine.block.setWidth(demoBlock, value: 220) try engine.block.setHeight(demoBlock, value: 220) try engine.block.setPositionX(demoBlock, value: 40) try engine.block.setPositionY(demoBlock, value: 40) try engine.block.appendChild(to: page, child: demoBlock) // ## Accessing Shapes let supportsShapes = try engine.block.supportsShape(demoBlock) let shape = try engine.block.getShape(demoBlock) let shapeType = try engine.block.getType(shape) print("Supports shape: \(supportsShapes), shape type: \(shapeType)") // ## Changing Shape Type // Hold a reference to the old shape, swap it for the new one, then destroy // the old shape so it doesn't leak. let oldShape = try engine.block.getShape(demoBlock) let ellipseShape = try engine.block.createShape(.ellipse) try engine.block.setShape(demoBlock, shape: ellipseShape) try engine.block.destroy(oldShape) try await engine.captureGuide(page, label: "after-shape-replace") // ## Discovering Shape Properties // Each shape type exposes its own property keys. Use findAllProperties to // list them. let starBlock = try engine.block.create(.graphic) let starShape = try engine.block.createShape(.star) try engine.block.setShape(starBlock, shape: starShape) let starFill = try engine.block.createFill(.color) try engine.block.setColor( starFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.55, b: 0.35, a: 1.0), ) try engine.block.setFill(starBlock, fill: starFill) try engine.block.setWidth(starBlock, value: 200) try engine.block.setHeight(starBlock, value: 200) try engine.block.setPositionX(starBlock, value: 290) try engine.block.setPositionY(starBlock, value: 40) try engine.block.appendChild(to: page, child: starBlock) let starProperties = try engine.block.findAllProperties(starShape) print("Star properties: \(starProperties)") // Prints: ["includedInExport", "name", "shape/star/cornerRadius", "shape/star/innerDiameter", "shape/star/points", "type", "uuid"] // ### Star Properties try engine.block.setInt(starShape, property: "shape/star/points", value: 7) try engine.block.setFloat(starShape, property: "shape/star/innerDiameter", value: 0.45) // ### Rectangle Corner Radii // Each corner has its own property — set them independently. let roundedBlock = try engine.block.create(.graphic) let roundedShape = try engine.block.createShape(.rect) try engine.block.setShape(roundedBlock, shape: roundedShape) let roundedFill = try engine.block.createFill(.color) try engine.block.setColor( roundedFill, property: "fill/color/value", color: .rgba(r: 0.42, g: 0.66, b: 0.94, a: 1.0), ) try engine.block.setFill(roundedBlock, fill: roundedFill) try engine.block.setWidth(roundedBlock, value: 200) try engine.block.setHeight(roundedBlock, value: 160) try engine.block.setPositionX(roundedBlock, value: 540) try engine.block.setPositionY(roundedBlock, value: 60) try engine.block.appendChild(to: page, child: roundedBlock) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusTL", value: 40) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusTR", value: 40) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusBR", value: 40) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusBL", value: 40) // ### Polygon Properties let polygonBlock = try engine.block.create(.graphic) let polygonShape = try engine.block.createShape(.polygon) try engine.block.setShape(polygonBlock, shape: polygonShape) let polygonFill = try engine.block.createFill(.color) try engine.block.setColor( polygonFill, property: "fill/color/value", color: .rgba(r: 0.20, g: 0.65, b: 0.55, a: 1.0), ) try engine.block.setFill(polygonBlock, fill: polygonFill) try engine.block.setWidth(polygonBlock, value: 160) try engine.block.setHeight(polygonBlock, value: 160) try engine.block.setPositionX(polygonBlock, value: 40) try engine.block.setPositionY(polygonBlock, value: 290) try engine.block.appendChild(to: page, child: polygonBlock) try engine.block.setInt(polygonShape, property: "shape/polygon/sides", value: 6) // ### Line // Lines have no shape-specific properties. Their visual thickness comes // from the parent block's stroke. let lineBlock = try engine.block.create(.graphic) try engine.block.setShape(lineBlock, shape: engine.block.createShape(.line)) try engine.block.setStrokeEnabled(lineBlock, enabled: true) try engine.block.setStrokeColor(lineBlock, color: .rgba(r: 0.15, g: 0.15, b: 0.15, a: 1.0)) try engine.block.setStrokeWidth(lineBlock, width: 4) try engine.block.setWidth(lineBlock, value: 160) try engine.block.setHeight(lineBlock, value: 8) try engine.block.setPositionX(lineBlock, value: 40) try engine.block.setPositionY(lineBlock, value: 470) try engine.block.appendChild(to: page, child: lineBlock) // ### Vector Path let vectorPathBlock = try engine.block.create(.graphic) let vectorPathShape = try engine.block.createShape(.vectorPath) try engine.block.setShape(vectorPathBlock, shape: vectorPathShape) let vectorPathFill = try engine.block.createFill(.color) try engine.block.setColor( vectorPathFill, property: "fill/color/value", color: .rgba(r: 0.55, g: 0.35, b: 0.85, a: 1.0), ) try engine.block.setFill(vectorPathBlock, fill: vectorPathFill) try engine.block.setWidth(vectorPathBlock, value: 160) try engine.block.setHeight(vectorPathBlock, value: 160) try engine.block.setPositionX(vectorPathBlock, value: 230) try engine.block.setPositionY(vectorPathBlock, value: 290) try engine.block.appendChild(to: page, child: vectorPathBlock) // Single-path SVG-style path data (a heart shape). try engine.block.setString( vectorPathShape, property: "shape/vector_path/path", value: "M 50,15 C 35,-5 5,5 5,30 C 5,55 30,75 50,95 C 70,75 95,55 95,30 C 95,5 65,-5 50,15 Z", ) // ## Editing Fill Color // Read the existing fill from the demo block and update its color. let fill = try engine.block.getFill(demoBlock) try engine.block.setColor( fill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.30, b: 0.45, a: 1.0), ) try await engine.captureGuide(page, label: "after-color-change") // ## Replacing Fill Type // Build a linear-gradient fill, then swap the demo block's color fill for it. let gradientFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( gradientFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.95, g: 0.30, b: 0.45, a: 1.0), stop: 0.0), GradientColorStop(color: .rgba(r: 0.85, g: 0.55, b: 0.95, a: 1.0), stop: 1.0), ], ) // Destroy the previous fill before swapping so it doesn't leak. try engine.block.destroy(engine.block.getFill(demoBlock)) try engine.block.setFill(demoBlock, fill: gradientFill) // ## Editing Stroke Properties if try engine.block.supportsStroke(demoBlock) { try engine.block.setStrokeEnabled(demoBlock, enabled: true) try engine.block.setStrokeColor(demoBlock, color: .rgba(r: 0.10, g: 0.10, b: 0.30, a: 1.0)) try engine.block.setStrokeWidth(demoBlock, width: 6) try engine.block.setStrokePosition(demoBlock, position: .outer) } try await engine.captureGuide(page, label: "after-stroke") // ## Transform Operations // Position and dimensions move the block around the page; rotation expects // radians; flips mirror the rendered content. try engine.block.setPositionX(demoBlock, value: 40) try engine.block.setPositionY(demoBlock, value: 40) try engine.block.setWidth(demoBlock, value: 220) try engine.block.setHeight(demoBlock, value: 220) try engine.block.setRotation(demoBlock, radians: .pi / 12) try engine.block.setFlipHorizontal(demoBlock, flip: true) try await engine.captureGuide(page, label: "after-transform") // ## Combining Shapes with Boolean Operations // Create two overlapping ellipses, then combine them into one graphic. let booleanA = try engine.block.create(.graphic) try engine.block.setShape(booleanA, shape: engine.block.createShape(.ellipse)) let booleanFillA = try engine.block.createFill(.color) try engine.block.setColor( booleanFillA, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.45, b: 0.75, a: 1.0), ) try engine.block.setFill(booleanA, fill: booleanFillA) try engine.block.setWidth(booleanA, value: 140) try engine.block.setHeight(booleanA, value: 140) try engine.block.setPositionX(booleanA, value: 420) try engine.block.setPositionY(booleanA, value: 310) try engine.block.appendChild(to: page, child: booleanA) let booleanB = try engine.block.create(.graphic) try engine.block.setShape(booleanB, shape: engine.block.createShape(.ellipse)) let booleanFillB = try engine.block.createFill(.color) try engine.block.setColor( booleanFillB, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.45, b: 0.75, a: 1.0), ) try engine.block.setFill(booleanB, fill: booleanFillB) try engine.block.setWidth(booleanB, value: 140) try engine.block.setHeight(booleanB, value: 140) try engine.block.setPositionX(booleanB, value: 510) try engine.block.setPositionY(booleanB, value: 360) try engine.block.appendChild(to: page, child: booleanB) // combine() destroys the input blocks and returns a new graphic that carries // a vector_path shape with the merged geometry. let union = try engine.block.combine([booleanA, booleanB], booleanOperation: .union) print("Union block: \(union)") // ## Applying Effects to Shapes let effectBlock = try engine.block.create(.graphic) try engine.block.setShape(effectBlock, shape: engine.block.createShape(.rect)) let effectFill = try engine.block.createFill(.color) try engine.block.setColor( effectFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.85, b: 0.30, a: 1.0), ) try engine.block.setFill(effectBlock, fill: effectFill) try engine.block.setWidth(effectBlock, value: 200) try engine.block.setHeight(effectBlock, value: 80) try engine.block.setPositionX(effectBlock, value: 290) try engine.block.setPositionY(effectBlock, value: 500) try engine.block.appendChild(to: page, child: effectBlock) let extrudeBlur = try engine.block.createEffect(.extrudeBlur) try engine.block.setFloat(extrudeBlur, property: "effect/extrude_blur/amount", value: 0.8) try engine.block.appendEffect(effectBlock, effectID: extrudeBlur) // ## Grouping and Ungrouping let groupChildA = try engine.block.create(.graphic) try engine.block.setShape(groupChildA, shape: engine.block.createShape(.rect)) let groupFillA = try engine.block.createFill(.color) try engine.block.setColor( groupFillA, property: "fill/color/value", color: .rgba(r: 0.42, g: 0.66, b: 0.94, a: 1.0), ) try engine.block.setFill(groupChildA, fill: groupFillA) try engine.block.setWidth(groupChildA, value: 80) try engine.block.setHeight(groupChildA, value: 80) try engine.block.setPositionX(groupChildA, value: 520) try engine.block.setPositionY(groupChildA, value: 500) try engine.block.appendChild(to: page, child: groupChildA) let groupChildB = try engine.block.create(.graphic) try engine.block.setShape(groupChildB, shape: engine.block.createShape(.ellipse)) let groupFillB = try engine.block.createFill(.color) try engine.block.setColor( groupFillB, property: "fill/color/value", color: .rgba(r: 0.20, g: 0.65, b: 0.55, a: 1.0), ) try engine.block.setFill(groupChildB, fill: groupFillB) try engine.block.setWidth(groupChildB, value: 80) try engine.block.setHeight(groupChildB, value: 80) try engine.block.setPositionX(groupChildB, value: 620) try engine.block.setPositionY(groupChildB, value: 500) try engine.block.appendChild(to: page, child: groupChildB) let canGroup = try engine.block.isGroupable([groupChildA, groupChildB]) if canGroup { let groupBlock = try engine.block.group([groupChildA, groupChildB]) print("Group container: \(groupBlock)") // Call ungroup() to dissolve the container and re-parent the children: // try engine.block.ungroup(groupBlock) } try await engine.captureGuide(page, label: "hero") } ``` Edit graphic block shapes through the Engine API — replace geometry, modify shape-specific properties, change fills and strokes, transform, combine, and group. ![A gallery of edited graphic blocks — a rotated and flipped pink-purple gradient ellipse with a dark outer stroke, an orange seven-point star, a blue rounded rectangle, a teal hexagon, a purple heart-shaped vector path, a black line, a pink union of two ellipses, a yellow rectangle with an extrude effect applied, and a blue-and-teal grouped pair](./assets/swift-based.hero.webp) > **Reading time:** 20 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-edit-shapes) The `graphic` [design block](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) in CE.SDK pairs a shape — the geometric definition — with a fill — the color, gradient, image, or video that makes the shape visible. This guide covers editing every part of that pair: swapping the shape's geometry, modifying shape-specific properties, changing fills and strokes, transforming the block, combining shapes with boolean operations, applying effects, and grouping. ```swift highlight-editShapes-setup // Start from a graphic block with a rectangle shape and a solid color fill. let demoBlock = try engine.block.create(.graphic) try engine.block.setShape(demoBlock, shape: engine.block.createShape(.rect)) let demoFill = try engine.block.createFill(.color) try engine.block.setColor( demoFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.85, b: 0.30, a: 1.0), ) try engine.block.setFill(demoBlock, fill: demoFill) try engine.block.setWidth(demoBlock, value: 220) try engine.block.setHeight(demoBlock, value: 220) try engine.block.setPositionX(demoBlock, value: 40) try engine.block.setPositionY(demoBlock, value: 40) try engine.block.appendChild(to: page, child: demoBlock) ``` ## Accessing Shapes Query whether a block supports shapes with `supportsShape(_:)`. Only graphic blocks support shapes — text, scene, and page blocks do not. Read the attached shape with `getShape(_:)`, then inspect it with the same APIs as any block — `getType(_:)`, `findAllProperties(_:)`, the typed getters. ```swift highlight-editShapes-accessShape let supportsShapes = try engine.block.supportsShape(demoBlock) let shape = try engine.block.getShape(demoBlock) let shapeType = try engine.block.getType(shape) print("Supports shape: \(supportsShapes), shape type: \(shapeType)") ``` ## Changing Shape Type Replace a shape by creating the new type with `createShape(_:)` and assigning it to the block with `setShape(_:shape:)`. The previously attached shape is **not** destroyed automatically — hold a reference to it, swap in the new shape, then call `destroy(_:)` on the old one so it doesn't leak. Destroying the parent graphic destroys its attached shape as well. ```swift highlight-editShapes-replaceShape // Hold a reference to the old shape, swap it for the new one, then destroy // the old shape so it doesn't leak. let oldShape = try engine.block.getShape(demoBlock) let ellipseShape = try engine.block.createShape(.ellipse) try engine.block.setShape(demoBlock, shape: ellipseShape) try engine.block.destroy(oldShape) ``` ## Discovering Shape Properties Every shape type exposes its own set of property keys. Use `findAllProperties(_:)` to list them, then read or write with the typed setters that match the value type — `setInt(_:property:value:)` for integers, `setFloat(_:property:value:)` for floats, `setString(_:property:value:)` for strings. ```swift highlight-editShapes-discoverProperties // Each shape type exposes its own property keys. Use findAllProperties to // list them. let starBlock = try engine.block.create(.graphic) let starShape = try engine.block.createShape(.star) try engine.block.setShape(starBlock, shape: starShape) let starFill = try engine.block.createFill(.color) try engine.block.setColor( starFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.55, b: 0.35, a: 1.0), ) try engine.block.setFill(starBlock, fill: starFill) try engine.block.setWidth(starBlock, value: 200) try engine.block.setHeight(starBlock, value: 200) try engine.block.setPositionX(starBlock, value: 290) try engine.block.setPositionY(starBlock, value: 40) try engine.block.appendChild(to: page, child: starBlock) let starProperties = try engine.block.findAllProperties(starShape) print("Star properties: \(starProperties)") // Prints: ["includedInExport", "name", "shape/star/cornerRadius", "shape/star/innerDiameter", "shape/star/points", "type", "uuid"] ``` ### Star Properties `shape/star/points` (Int) controls the number of points. `shape/star/innerDiameter` (Float) is the inner-to-outer radius ratio — values closer to `0` make the points sharper, values closer to `1` make the star round. ```swift highlight-editShapes-starProperties try engine.block.setInt(starShape, property: "shape/star/points", value: 7) try engine.block.setFloat(starShape, property: "shape/star/innerDiameter", value: 0.45) ``` ### Rectangle Corner Radii Rectangle corners are stored as four independent floats: `shape/rect/cornerRadiusTL`, `cornerRadiusTR`, `cornerRadiusBR`, and `cornerRadiusBL`. Set each corner explicitly when you need rounded corners — there is no single uniform property. ```swift highlight-editShapes-rectProperties // Each corner has its own property — set them independently. let roundedBlock = try engine.block.create(.graphic) let roundedShape = try engine.block.createShape(.rect) try engine.block.setShape(roundedBlock, shape: roundedShape) let roundedFill = try engine.block.createFill(.color) try engine.block.setColor( roundedFill, property: "fill/color/value", color: .rgba(r: 0.42, g: 0.66, b: 0.94, a: 1.0), ) try engine.block.setFill(roundedBlock, fill: roundedFill) try engine.block.setWidth(roundedBlock, value: 200) try engine.block.setHeight(roundedBlock, value: 160) try engine.block.setPositionX(roundedBlock, value: 540) try engine.block.setPositionY(roundedBlock, value: 60) try engine.block.appendChild(to: page, child: roundedBlock) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusTL", value: 40) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusTR", value: 40) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusBR", value: 40) try engine.block.setFloat(roundedShape, property: "shape/rect/cornerRadiusBL", value: 40) ``` ### Polygon Properties `shape/polygon/sides` (Int) controls the number of sides. ```swift highlight-editShapes-polygonProperties let polygonBlock = try engine.block.create(.graphic) let polygonShape = try engine.block.createShape(.polygon) try engine.block.setShape(polygonBlock, shape: polygonShape) let polygonFill = try engine.block.createFill(.color) try engine.block.setColor( polygonFill, property: "fill/color/value", color: .rgba(r: 0.20, g: 0.65, b: 0.55, a: 1.0), ) try engine.block.setFill(polygonBlock, fill: polygonFill) try engine.block.setWidth(polygonBlock, value: 160) try engine.block.setHeight(polygonBlock, value: 160) try engine.block.setPositionX(polygonBlock, value: 40) try engine.block.setPositionY(polygonBlock, value: 290) try engine.block.appendChild(to: page, child: polygonBlock) try engine.block.setInt(polygonShape, property: "shape/polygon/sides", value: 6) ``` ### Line Line shapes expose no shape-specific properties — they render as a single horizontal segment whose visible thickness comes from the parent block's stroke. Width and height set on the parent block control the line's length and the box it occupies. ```swift highlight-editShapes-lineProperties // Lines have no shape-specific properties. Their visual thickness comes // from the parent block's stroke. let lineBlock = try engine.block.create(.graphic) try engine.block.setShape(lineBlock, shape: engine.block.createShape(.line)) try engine.block.setStrokeEnabled(lineBlock, enabled: true) try engine.block.setStrokeColor(lineBlock, color: .rgba(r: 0.15, g: 0.15, b: 0.15, a: 1.0)) try engine.block.setStrokeWidth(lineBlock, width: 4) try engine.block.setWidth(lineBlock, value: 160) try engine.block.setHeight(lineBlock, value: 8) try engine.block.setPositionX(lineBlock, value: 40) try engine.block.setPositionY(lineBlock, value: 470) try engine.block.appendChild(to: page, child: lineBlock) ``` ### Vector Path Vector path shapes accept the `d` attribute of an SVG `` element through `shape/vector_path/path` (String). Multiple subpaths within a single path string (separated by `M` move commands) are supported; loading a complete SVG document is not — extract the path data first. ```swift highlight-editShapes-vectorPath let vectorPathBlock = try engine.block.create(.graphic) let vectorPathShape = try engine.block.createShape(.vectorPath) try engine.block.setShape(vectorPathBlock, shape: vectorPathShape) let vectorPathFill = try engine.block.createFill(.color) try engine.block.setColor( vectorPathFill, property: "fill/color/value", color: .rgba(r: 0.55, g: 0.35, b: 0.85, a: 1.0), ) try engine.block.setFill(vectorPathBlock, fill: vectorPathFill) try engine.block.setWidth(vectorPathBlock, value: 160) try engine.block.setHeight(vectorPathBlock, value: 160) try engine.block.setPositionX(vectorPathBlock, value: 230) try engine.block.setPositionY(vectorPathBlock, value: 290) try engine.block.appendChild(to: page, child: vectorPathBlock) // Single-path SVG-style path data (a heart shape). try engine.block.setString( vectorPathShape, property: "shape/vector_path/path", value: "M 50,15 C 35,-5 5,5 5,30 C 5,55 30,75 50,95 C 70,75 95,55 95,30 C 95,5 65,-5 50,15 Z", ) ``` ## Editing Fill Color The graphic block's fill is a separate design block. Read it with `getFill(_:)`, then set its `fill/color/value` property using a typed `Color` — `.rgba(r:g:b:a:)` for sRGB, `.cmyk(c:m:y:k:tint:)` for CMYK, or `.spot(name:tint:externalReference:)` for spot colors. ```swift highlight-editShapes-fillColor // Read the existing fill from the demo block and update its color. let fill = try engine.block.getFill(demoBlock) try engine.block.setColor( fill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.30, b: 0.45, a: 1.0), ) ``` For more on color fills, see [Color Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/color-7129cd/). ## Replacing Fill Type Swap a fill type — color, gradient, image, video — by creating a new fill with `createFill(_:)` and assigning it with `setFill(_:fill:)`. Destroy the previous fill so it doesn't leak. ```swift highlight-editShapes-replaceFill // Build a linear-gradient fill, then swap the demo block's color fill for it. let gradientFill = try engine.block.createFill(.linearGradient) try engine.block.setGradientColorStops( gradientFill, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 0.95, g: 0.30, b: 0.45, a: 1.0), stop: 0.0), GradientColorStop(color: .rgba(r: 0.85, g: 0.55, b: 0.95, a: 1.0), stop: 1.0), ], ) // Destroy the previous fill before swapping so it doesn't leak. try engine.block.destroy(engine.block.getFill(demoBlock)) try engine.block.setFill(demoBlock, fill: gradientFill) ``` For gradient fills in depth, see [Gradient Fills](https://img.ly/docs/cesdk/mac-catalyst/fills/gradient-0ff079/). ## Editing Stroke Properties Strokes are off by default. Enable with `setStrokeEnabled(_:enabled:)`, then configure color (`setStrokeColor(_:color:)`), width (`setStrokeWidth(_:width:)`), and the position where the stroke sits relative to the shape's edge (`setStrokePosition(_:position:)` with `.inner`, `.center`, or `.outer`). ```swift highlight-editShapes-stroke if try engine.block.supportsStroke(demoBlock) { try engine.block.setStrokeEnabled(demoBlock, enabled: true) try engine.block.setStrokeColor(demoBlock, color: .rgba(r: 0.10, g: 0.10, b: 0.30, a: 1.0)) try engine.block.setStrokeWidth(demoBlock, width: 6) try engine.block.setStrokePosition(demoBlock, position: .outer) } ``` ## Transform Operations Position and dimensions move the block around the page; rotation expects radians; flips mirror the rendered content along the horizontal or vertical axis. ```swift highlight-editShapes-transforms // Position and dimensions move the block around the page; rotation expects // radians; flips mirror the rendered content. try engine.block.setPositionX(demoBlock, value: 40) try engine.block.setPositionY(demoBlock, value: 40) try engine.block.setWidth(demoBlock, value: 220) try engine.block.setHeight(demoBlock, value: 220) try engine.block.setRotation(demoBlock, radians: .pi / 12) try engine.block.setFlipHorizontal(demoBlock, flip: true) ``` ## Combining Shapes with Boolean Operations `combine(_:booleanOperation:)` merges two or more graphic or text blocks on the same page into a single new graphic carrying a vector path shape with the merged geometry. The input blocks are destroyed. Available operations on `BooleanOperation`: - `.union` — adds all input shapes into one. - `.difference` — removes the upper shapes from the bottom-most shape. - `.intersection` — keeps only the area where every input overlaps. - `.xor` — keeps the non-overlapping parts. ```swift highlight-editShapes-booleanCombine // Create two overlapping ellipses, then combine them into one graphic. let booleanA = try engine.block.create(.graphic) try engine.block.setShape(booleanA, shape: engine.block.createShape(.ellipse)) let booleanFillA = try engine.block.createFill(.color) try engine.block.setColor( booleanFillA, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.45, b: 0.75, a: 1.0), ) try engine.block.setFill(booleanA, fill: booleanFillA) try engine.block.setWidth(booleanA, value: 140) try engine.block.setHeight(booleanA, value: 140) try engine.block.setPositionX(booleanA, value: 420) try engine.block.setPositionY(booleanA, value: 310) try engine.block.appendChild(to: page, child: booleanA) let booleanB = try engine.block.create(.graphic) try engine.block.setShape(booleanB, shape: engine.block.createShape(.ellipse)) let booleanFillB = try engine.block.createFill(.color) try engine.block.setColor( booleanFillB, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.45, b: 0.75, a: 1.0), ) try engine.block.setFill(booleanB, fill: booleanFillB) try engine.block.setWidth(booleanB, value: 140) try engine.block.setHeight(booleanB, value: 140) try engine.block.setPositionX(booleanB, value: 510) try engine.block.setPositionY(booleanB, value: 360) try engine.block.appendChild(to: page, child: booleanB) // combine() destroys the input blocks and returns a new graphic that carries // a vector_path shape with the merged geometry. let union = try engine.block.combine([booleanA, booleanB], booleanOperation: .union) print("Union block: \(union)") ``` For a deeper look at boolean operations, see [Combine Stickers and Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/combine-2a9e26/). ## Applying Effects to Shapes Effects — blurs, shadows, filters, extrusions — attach to graphic blocks. Create the effect with `createEffect(_:)` using an `EffectType` case, configure its parameters with the typed setters (use `findAllProperties(_:)` to discover the property keys), then append it to the block with `appendEffect(_:effectID:)`. A block can carry multiple effects. ```swift highlight-editShapes-effect let effectBlock = try engine.block.create(.graphic) try engine.block.setShape(effectBlock, shape: engine.block.createShape(.rect)) let effectFill = try engine.block.createFill(.color) try engine.block.setColor( effectFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.85, b: 0.30, a: 1.0), ) try engine.block.setFill(effectBlock, fill: effectFill) try engine.block.setWidth(effectBlock, value: 200) try engine.block.setHeight(effectBlock, value: 80) try engine.block.setPositionX(effectBlock, value: 290) try engine.block.setPositionY(effectBlock, value: 500) try engine.block.appendChild(to: page, child: effectBlock) let extrudeBlur = try engine.block.createEffect(.extrudeBlur) try engine.block.setFloat(extrudeBlur, property: "effect/extrude_blur/amount", value: 0.8) try engine.block.appendEffect(effectBlock, effectID: extrudeBlur) ``` ## Grouping and Ungrouping `group(_:)` consumes an array of blocks and returns a new group container that transforms them together. `ungroup(_:)` dissolves the container and re-parents the children back to the group's parent. Check whether blocks are eligible to be grouped with `isGroupable(_:)` — all blocks must be on the same page (or none of them on any page), none of them can already be inside another group, and pages and scenes cannot be grouped. ```swift highlight-editShapes-group let groupChildA = try engine.block.create(.graphic) try engine.block.setShape(groupChildA, shape: engine.block.createShape(.rect)) let groupFillA = try engine.block.createFill(.color) try engine.block.setColor( groupFillA, property: "fill/color/value", color: .rgba(r: 0.42, g: 0.66, b: 0.94, a: 1.0), ) try engine.block.setFill(groupChildA, fill: groupFillA) try engine.block.setWidth(groupChildA, value: 80) try engine.block.setHeight(groupChildA, value: 80) try engine.block.setPositionX(groupChildA, value: 520) try engine.block.setPositionY(groupChildA, value: 500) try engine.block.appendChild(to: page, child: groupChildA) let groupChildB = try engine.block.create(.graphic) try engine.block.setShape(groupChildB, shape: engine.block.createShape(.ellipse)) let groupFillB = try engine.block.createFill(.color) try engine.block.setColor( groupFillB, property: "fill/color/value", color: .rgba(r: 0.20, g: 0.65, b: 0.55, a: 1.0), ) try engine.block.setFill(groupChildB, fill: groupFillB) try engine.block.setWidth(groupChildB, value: 80) try engine.block.setHeight(groupChildB, value: 80) try engine.block.setPositionX(groupChildB, value: 620) try engine.block.setPositionY(groupChildB, value: 500) try engine.block.appendChild(to: page, child: groupChildB) let canGroup = try engine.block.isGroupable([groupChildA, groupChildB]) if canGroup { let groupBlock = try engine.block.group([groupChildA, groupChildB]) print("Group container: \(groupBlock)") // Call ungroup() to dissolve the container and re-parent the children: // try engine.block.ungroup(groupBlock) } ``` ## Troubleshooting ### Shape Not Changing - Verify `supportsShape(_:)` returns `true` for the block — only graphic blocks support shapes. - Confirm `setShape(_:shape:)` was called on the graphic block, not on the shape block. - Destroy the previous shape only after `setShape(_:shape:)` returns, so the block never holds a reference to a destroyed shape. ### Property Modification Not Working - Use `findAllProperties(_:)` on the shape to confirm the property key exists. - Match the setter to the property type — `setInt(_:property:value:)` for integer properties, `setFloat(_:property:value:)` for floats, `setString(_:property:value:)` for strings. - Remember that shape-specific properties change when you replace one shape type with another — `shape/star/points` only exists on star shapes. ### Fill Not Visible - Confirm the fill is enabled with `isFillEnabled(_:)`. - Verify the fill's color has non-zero alpha. - Confirm the block has non-zero width and height. ### Boolean Operation Fails - Pass at least two blocks — `combine(_:booleanOperation:)` rejects fewer than two. - Each input must be a graphic or text block; other block types are rejected. - All inputs must resolve to the same page in the scene hierarchy. ### Stroke Not Appearing - Confirm `setStrokeEnabled(_:enabled:)` was called with `true`. - Verify the stroke width is greater than zero. - Verify the stroke color has non-zero alpha. - A center- or inner-positioned stroke can be hidden behind the fill on small shapes — try `.outer`. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.supportsShape(_:)` | Check whether a block can carry a shape | | `engine.block.getShape(_:)` | Get the shape attached to a graphic block | | `engine.block.createShape(_:)` | Create a new shape of a given `ShapeType` | | `engine.block.setShape(_:shape:)` | Attach a shape to a graphic block | | `engine.block.getType(_:)` | Read the type identifier of a block or shape | | `engine.block.findAllProperties(_:)` | List the property keys exposed by a block, shape, fill, or effect | | `engine.block.setInt(_:property:value:)` | Set an integer property (e.g. star points, polygon sides) | | `engine.block.setFloat(_:property:value:)` | Set a float property (e.g. corner radius, inner diameter) | | `engine.block.setString(_:property:value:)` | Set a string property (e.g. vector path data) | | `engine.block.getFill(_:)` | Get the fill attached to a graphic block | | `engine.block.createFill(_:)` | Create a new fill of a given `FillType` | | `engine.block.setFill(_:fill:)` | Attach a fill to a graphic block | | `engine.block.setColor(_:property:color:)` | Set a color property using a typed `Color` | | `engine.block.setGradientColorStops(_:property:colors:)` | Set the gradient stops on a gradient fill | | `engine.block.supportsStroke(_:)` | Check whether a block supports a stroke | | `engine.block.setStrokeEnabled(_:enabled:)` | Toggle the stroke on or off | | `engine.block.setStrokeColor(_:color:)` | Set the stroke color | | `engine.block.setStrokeWidth(_:width:)` | Set the stroke width | | `engine.block.setStrokePosition(_:position:)` | Set the stroke position relative to the shape edge | | `engine.block.setPositionX(_:value:)` / `setPositionY(_:value:)` | Move the block on the page | | `engine.block.setWidth(_:value:)` / `setHeight(_:value:)` | Resize the block | | `engine.block.setRotation(_:radians:)` | Rotate the block around its center | | `engine.block.setFlipHorizontal(_:flip:)` | Mirror the block horizontally | | `engine.block.combine(_:booleanOperation:)` | Combine graphic blocks with a `BooleanOperation` | | `engine.block.createEffect(_:)` | Create an effect of a given `EffectType` | | `engine.block.appendEffect(_:effectID:)` | Attach an effect to a graphic block | | `engine.block.isGroupable(_:)` | Check whether a set of blocks can be grouped | | `engine.block.group(_:)` | Group blocks on the same page into a single container | | `engine.block.ungroup(_:)` | Dissolve a group container | | `engine.block.destroy(_:)` | Destroy a block, shape, fill, or effect | ### Properties | Property | Type | Description | | --- | --- | --- | | `fill/color/value` | Color | Solid color of a color fill | | `fill/gradient/colors` | `[GradientColorStop]` | Color stops on a gradient fill | | `shape/rect/cornerRadiusTL` | Float | Top-left corner radius | | `shape/rect/cornerRadiusTR` | Float | Top-right corner radius | | `shape/rect/cornerRadiusBR` | Float | Bottom-right corner radius | | `shape/rect/cornerRadiusBL` | Float | Bottom-left corner radius | | `shape/star/points` | Int | Number of points on a star shape | | `shape/star/innerDiameter` | Float | Inner-to-outer radius ratio of a star (closer to `0` = sharper points) | | `shape/polygon/sides` | Int | Number of sides on a polygon | | `shape/vector_path/path` | String | SVG-style path data for a vector path shape | ## Next Steps - [Create Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-shapes-64acc0/) — Create and configure geometric shapes programmatically. - [Edit Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-stickers-609679/) — Edit existing sticker blocks by changing their image fill and transforms. --- ## 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 Stickers" description: "Modify stickers programmatically — swap the image, transform, restyle with shadows or strokes, and duplicate." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-stickers-609679/" --- > 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 Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-3d4e5f/) > [Edit Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-stickers-609679/) --- ```swift file=@cesdk_swift_examples/engine-guides-edit-stickers/EditStickers.swift reference-only import Foundation import IMGLYEngine @MainActor func editStickers(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL // Demo scaffolding: build a scene that already contains a sticker so the // edit sections below have something to act on. The recipe mirrors the // Create Stickers guide — a graphic block, a rect shape, an image fill, // and the `"sticker"` kind tag. let scene = try engine.scene.create() let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 450) try engine.block.setHeight(page, value: 250) try engine.block.appendChild(to: scene, child: page) let sticker = try engine.block.create(.graphic) try engine.block.setShape(sticker, shape: try engine.block.createShape(.rect)) let stickerFill = try engine.block.createFill(.image) try engine.block.setURL( stickerFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent( "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_grin.svg", ), ) try engine.block.setFill(sticker, fill: stickerFill) if try engine.block.supportsContentFillMode(sticker) { try engine.block.setContentFillMode(sticker, mode: .contain) } try engine.block.setKind(sticker, kind: "sticker") try engine.block.setWidth(sticker, value: 120) try engine.block.setHeight(sticker, value: 120) try engine.block.setPositionX(sticker, value: 50) try engine.block.setPositionY(sticker, value: 65) try engine.block.appendChild(to: page, child: sticker) let fill = try engine.block.getFill(sticker) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent( "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_blush.svg", ), ) try engine.block.setPositionX(sticker, value: 50) try engine.block.setPositionY(sticker, value: 65) try engine.block.setWidth(sticker, value: 120) try engine.block.setHeight(sticker, value: 120) try engine.block.setRotation(sticker, radians: .pi / 12) try engine.block.setFlipHorizontal(sticker, flip: false) if try engine.block.supportsContentFillMode(sticker) { try engine.block.setContentFillMode(sticker, mode: .cover) } // Reset to the recommended .contain mode for the rest of the demo. if try engine.block.supportsContentFillMode(sticker) { try engine.block.setContentFillMode(sticker, mode: .contain) } try engine.block.setOpacity(sticker, value: 1.0) try engine.block.setDropShadowEnabled(sticker, enabled: true) try engine.block.setDropShadowColor(sticker, color: .rgba(r: 0, g: 0, b: 0, a: 0.4)) try engine.block.setDropShadowOffsetX(sticker, offsetX: 4) try engine.block.setDropShadowOffsetY(sticker, offsetY: 4) try engine.block.setDropShadowBlurRadiusX(sticker, blurRadiusX: 6) try engine.block.setDropShadowBlurRadiusY(sticker, blurRadiusY: 6) let copy = try engine.block.duplicate(sticker) try engine.block.setPositionX(copy, value: 280) try engine.block.setPositionY(copy, value: 65) try await engine.captureGuide(page, label: "hero") try engine.block.setStrokeEnabled(sticker, enabled: true) try engine.block.setStrokeColor(sticker, color: .rgba(r: 1, g: 1, b: 1, a: 1)) try engine.block.setStrokeWidth(sticker, width: 4) let blur = try engine.block.createBlur(.uniform) try engine.block.setBlur(sticker, blurID: blur) try engine.block.setBlurEnabled(sticker, enabled: true) let tiltShift = try engine.block.createEffect(.tiltShift) try engine.block.appendEffect(sticker, effectID: tiltShift) } ``` Edit stickers after they've been placed in a scene — swap the source image, transform the block, restyle with shadows or strokes, and duplicate the configuration. ![Two blush emoticon stickers tilted slightly and rendered with soft drop shadows, side by side on the page.](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-edit-stickers) Stickers are graphic blocks with an image fill and the `"sticker"` kind tag (see [Create Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-stickers-cc46e5/)). Once a sticker exists in a scene, every transform, opacity, drop shadow, stroke, blur, and effect setter that works on a graphic block also works on the sticker. The one thing that doesn't apply is recoloring — the image fill preserves the source artwork's colors. ## Replacing the Sticker Image Swap the source artwork by writing a new URI to the sticker's existing image fill. Read the fill with `getFill(_:)`, then update `fill/image/imageFileURI` via `setURL(_:property:value:)`. The block's transform, opacity, drop shadow, and kind tag stay the same — only the rendered image changes. ```swift highlight-editStickers-replaceImage let fill = try engine.block.getFill(sticker) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent( "ly.img.sticker/images/emoticons/imgly_sticker_emoticons_blush.svg", ), ) ``` Writing the URI on the **block** instead of its fill throws `BlockImageFillUnsupported` — `fill/image/imageFileURI` lives on the fill, not the block. Always target the fill returned by `getFill(_:)`. ## Transforming Stickers Position, size, rotation, and flip respond to the same setters as every other graphic block. Rotation is in radians, and position/dimensions are in the scene's `DesignUnit`. ```swift highlight-editStickers-transform try engine.block.setPositionX(sticker, value: 50) try engine.block.setPositionY(sticker, value: 65) try engine.block.setWidth(sticker, value: 120) try engine.block.setHeight(sticker, value: 120) try engine.block.setRotation(sticker, radians: .pi / 12) try engine.block.setFlipHorizontal(sticker, flip: false) ``` `setRotation(_:radians:)` accepts any finite radian value (non-finite values throw). `setFlipHorizontal(_:flip:)` and `setFlipVertical(_:flip:)` mirror the sticker without changing its position or dimensions. ## Changing the Content Fill Mode `ContentFillMode` controls how the image fits inside the block's bounds. `.contain` preserves the source aspect ratio (the recommended default for stickers). `.cover` stretches the image to fill the bounds, cropping if the aspect ratios differ. `.crop` enables manual cropping. ```swift highlight-editStickers-contentFillMode if try engine.block.supportsContentFillMode(sticker) { try engine.block.setContentFillMode(sticker, mode: .cover) } ``` Always gate the call on `supportsContentFillMode(_:)` — only blocks with content-bearing fills support the mode. ## Adjusting Opacity `setOpacity(_:value:)` accepts values from `0.0` (fully transparent) to `1.0` (fully opaque). Values outside that range — and non-finite values like `NaN` or infinity — throw an error. ```swift highlight-editStickers-opacity try engine.block.setOpacity(sticker, value: 1.0) ``` ## Adding a Drop Shadow Drop shadows are a property of the block itself, not the fill. Enable the shadow, then configure color, offset, and blur radius for each axis. The color form takes a typed `Color` value — use `.rgba(r:g:b:a:)` for a quick literal. ```swift highlight-editStickers-dropShadow try engine.block.setDropShadowEnabled(sticker, enabled: true) try engine.block.setDropShadowColor(sticker, color: .rgba(r: 0, g: 0, b: 0, a: 0.4)) try engine.block.setDropShadowOffsetX(sticker, offsetX: 4) try engine.block.setDropShadowOffsetY(sticker, offsetY: 4) try engine.block.setDropShadowBlurRadiusX(sticker, blurRadiusX: 6) try engine.block.setDropShadowBlurRadiusY(sticker, blurRadiusY: 6) ``` ## Duplicating Stickers `duplicate(_:)` produces an independent copy of the sticker — same image, dimensions, transform, drop shadow, content fill mode, and kind tag. The copy is attached to the same parent by default, so it appears on the same page. Change only the properties that should differ between original and copy. ```swift highlight-editStickers-duplicate let copy = try engine.block.duplicate(sticker) try engine.block.setPositionX(copy, value: 280) try engine.block.setPositionY(copy, value: 65) ``` ## Adding a Stroke Strokes draw an outline around the block bounds. Enable the stroke, then configure color, width, and (optionally) position and style. Stroke color uses the typed `Color` form. ```swift highlight-editStickers-stroke try engine.block.setStrokeEnabled(sticker, enabled: true) try engine.block.setStrokeColor(sticker, color: .rgba(r: 1, g: 1, b: 1, a: 1)) try engine.block.setStrokeWidth(sticker, width: 4) ``` Because the stroke follows the rect block bounds rather than the visible image silhouette, it produces a sharp rectangular outline around the sticker's bounding box. For non-rectangular stickers, prefer a drop shadow or a glow effect over a stroke. ## Applying Blur Attach a blur block to the sticker with `createBlur(_:)` and `setBlur(_:blurID:)`, then enable it with `setBlurEnabled(_:enabled:)`. Disabling the blur preserves the configuration so you can re-enable it without re-creating the blur block. ```swift highlight-editStickers-blur let blur = try engine.block.createBlur(.uniform) try engine.block.setBlur(sticker, blurID: blur) try engine.block.setBlurEnabled(sticker, enabled: true) ``` ## Applying Effects Effects compose with the block through `appendEffect(_:effectID:)`. Multiple effects stack in the order they were appended. ```swift highlight-editStickers-effects let tiltShift = try engine.block.createEffect(.tiltShift) try engine.block.appendEffect(sticker, effectID: tiltShift) ``` The full list of effect types lives in `EffectType` (`.tiltShift`, `.glow`, `.lutFilter`, `.pixelize`, `.posterize`, and more). ## Limitations Stickers preserve their source artwork's colors. As a consequence: - **Recoloring is not supported.** Calling `setColor` with `fill/solid/color` on a sticker throws `BlockFillSetSolidColorWrongType` — the fill is an image fill, not a solid color fill, so there is no color to overwrite. - **Boolean operations are not supported.** `combine(_:booleanOperation:)` requires vector path shapes; image fills cannot participate. If you need a recolorable or combinable graphic, create a shape instead — see [Edit Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-shapes-d67cfb/). ## Troubleshooting ### Sticker Image Did Not Change Verify the new URI is reachable and serves a supported image format (SVG, PNG, JPG). Confirm `setURL(_:property:value:)` was called on the **fill** returned by `getFill(_:)`, not on the block itself. ### Sticker Appears Stretched The default `.contain` mode preserves the source aspect ratio but leaves padding when the block's aspect ratio differs from the source artwork. Switch to `.cover` to fill the bounds, or resize the block to match the source aspect ratio. ### Drop Shadow or Stroke Not Visible Confirm the feature is enabled (`setDropShadowEnabled(_:enabled: true)`, `setStrokeEnabled(_:enabled: true)`). Check that the color's alpha component is non-zero and that stroke width and shadow blur radii are greater than zero. ### Sticker Cannot Be Recolored Expected — stickers use image fills, which preserve the source artwork's colors. For a recolorable graphic, build a shape with a color or gradient fill instead. ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `engine.block.getFill(_:)` | Fill | Get the image fill attached to a sticker | | `engine.block.setURL(_:property:value:)` | Fill | Write a new `fill/image/imageFileURI` as a typed `URL` | | `engine.block.setString(_:property:value:)` | Fill | Write a new image URI as a `String` | | `engine.block.supportsContentFillMode(_:)` | Content | Check whether the block supports a content fill mode | | `engine.block.setContentFillMode(_:mode:)` | Content | Switch between `.contain`, `.cover`, and `.crop` | | `engine.block.setPositionX/Y(_:value:)` | Transform | Move the sticker | | `engine.block.setWidth/Height(_:value:)` | Transform | Resize the sticker | | `engine.block.setRotation(_:radians:)` | Transform | Rotate the sticker (radians) | | `engine.block.setFlipHorizontal/Vertical(_:flip:)` | Transform | Mirror the sticker | | `engine.block.setOpacity(_:value:)` | Appearance | Set the sticker's opacity (`0.0` ... `1.0`) | | `engine.block.setDropShadowEnabled(_:enabled:)` | Drop shadow | Toggle the drop shadow | | `engine.block.setDropShadowColor(_:color:)` | Drop shadow | Set the drop shadow color | | `engine.block.setDropShadowOffsetX(_:offsetX:)` | Drop shadow | Offset the drop shadow on the X axis | | `engine.block.setDropShadowOffsetY(_:offsetY:)` | Drop shadow | Offset the drop shadow on the Y axis | | `engine.block.setDropShadowBlurRadiusX(_:blurRadiusX:)` | Drop shadow | Soften the drop shadow on the X axis | | `engine.block.setDropShadowBlurRadiusY(_:blurRadiusY:)` | Drop shadow | Soften the drop shadow on the Y axis | | `engine.block.setStrokeEnabled(_:enabled:)` | Stroke | Toggle the stroke | | `engine.block.setStrokeColor(_:color:)` | Stroke | Set the stroke color | | `engine.block.setStrokeWidth(_:width:)` | Stroke | Set the stroke width | | `engine.block.createBlur(_:)` | Blur | Create a blur block | | `engine.block.setBlur(_:blurID:)` | Blur | Attach a blur block to the sticker | | `engine.block.setBlurEnabled(_:enabled:)` | Blur | Toggle the attached blur | | `engine.block.createEffect(_:)` | Effects | Create an effect block | | `engine.block.appendEffect(_:effectID:)` | Effects | Attach an effect block to the sticker | | `engine.block.duplicate(_:)` | Lifecycle | Duplicate the sticker | ## Next Steps - [Create Stickers](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/create-stickers-cc46e5/) — Build stickers programmatically from image URLs. - [Edit Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/create-edit/edit-shapes-d67cfb/) — Modify recolorable, combinable shapes the same way. - [Combine Shapes](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/combine-2a9e26/) — Use boolean operations to merge vector shapes. --- ## 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: "Insert QR Code" description: "Generate a QR code with Core Image and insert it into a scene as an image fill, with positioning, sizing, and optional metadata for later updates." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/insert-qr-code-b6cc53/" --- > 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 Shapes](https://img.ly/docs/cesdk/mac-catalyst/shapes-9f1b2c/) > [Insert QR Code](https://img.ly/docs/cesdk/mac-catalyst/stickers-and-shapes/insert-qr-code-b6cc53/) --- ```swift file=@cesdk_swift_examples/engine-guides-shapes-qrcode/QRCodeGenerator.swift reference-only import CoreImage.CIFilterBuiltins import Foundation import IMGLYEngine #if canImport(UIKit) import UIKit private typealias PlatformColor = UIColor private typealias PlatformImage = UIImage #elseif canImport(AppKit) import AppKit private typealias PlatformColor = NSColor private typealias PlatformImage = NSImage #endif /// Generate a QR code image from a string using Core Image. /// - Parameters: /// - string: Content to encode; use a full URL with scheme. /// - correction: Error correction level (L, M, Q, H). "M" is a good default. /// - scale: Pixel scale factor. Increase for print. /// - foreground: Dark module color. /// - background: Light background color. private func makeQRCode( from string: String, correction: String = "M", scale: CGFloat = 10, foreground: PlatformColor = .black, background: PlatformColor = .white, ) throws -> PlatformImage { guard let data = string.data(using: .utf8) else { throw QRGenerationError.invalidInput } let qr = CIFilter.qrCodeGenerator() qr.setValue(data, forKey: "inputMessage") qr.setValue(correction, forKey: "inputCorrectionLevel") guard let output = qr.outputImage else { throw QRGenerationError.filterFailed } // Map the filter's default black-and-white output to the requested colors. let falseColor = CIFilter.falseColor() falseColor.inputImage = output #if canImport(UIKit) falseColor.color0 = CIColor(color: foreground) falseColor.color1 = CIColor(color: background) #elseif canImport(AppKit) falseColor.color0 = CIColor(color: foreground) ?? CIColor.black falseColor.color1 = CIColor(color: background) ?? CIColor.white #endif guard let colored = falseColor.outputImage else { throw QRGenerationError.filterFailed } // Scale without interpolation so QR modules stay crisp. let scaled = colored.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) let context = CIContext(options: [.useSoftwareRenderer: false]) guard let cg = context.createCGImage(scaled, from: scaled.extent) else { throw QRGenerationError.filterFailed } #if canImport(UIKit) return UIImage(cgImage: cg, scale: 1.0, orientation: .up) #elseif canImport(AppKit) return NSImage(cgImage: cg, size: NSSize(width: cg.width, height: cg.height)) #endif } private enum QRGenerationError: Error { case invalidInput case filterFailed case encodingFailed } @MainActor func insertQRCode(engine: Engine) async throws { // Demo scaffolding: create a scene and page so the QR block has a canvas. 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 qrImage = try makeQRCode(from: "https://img.ly") // Both PNG-encoding branches share the same output format. #if canImport(UIKit) guard let png = qrImage.pngData() else { throw QRGenerationError.encodingFailed } #elseif canImport(AppKit) guard let tiff = qrImage.tiffRepresentation, let bitmap = NSBitmapImageRep(data: tiff), let png = bitmap.representation(using: .png, properties: [:]) else { throw QRGenerationError.encodingFailed } #endif let qrFileURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("png") try png.write(to: qrFileURL) let qrBlock = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(qrBlock, shape: rectShape) let imageFill = try engine.block.createFill(.image) try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: qrFileURL) try engine.block.setFill(qrBlock, fill: imageFill) try engine.block.setWidth(qrBlock, value: 300) try engine.block.setHeight(qrBlock, value: 300) try engine.block.setPositionX(qrBlock, value: 250) try engine.block.setPositionY(qrBlock, value: 150) try engine.block.appendChild(to: page, child: qrBlock) try engine.block.setMetadata(qrBlock, key: "qr/url", value: "https://img.ly") try await engine.captureGuide(page, label: "hero") let updatedURL = "https://img.ly/showcases" let updatedImage = try makeQRCode(from: updatedURL) #if canImport(UIKit) guard let updatedPng = updatedImage.pngData() else { throw QRGenerationError.encodingFailed } #elseif canImport(AppKit) guard let updatedTiff = updatedImage.tiffRepresentation, let updatedBitmap = NSBitmapImageRep(data: updatedTiff), let updatedPng = updatedBitmap.representation(using: .png, properties: [:]) else { throw QRGenerationError.encodingFailed } #endif let updatedFileURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("png") try updatedPng.write(to: updatedFileURL) let fill = try engine.block.getFill(qrBlock) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: updatedFileURL) try engine.block.setMetadata(qrBlock, key: "qr/url", value: updatedURL) } ``` Generate a QR code with Core Image and place it on a CE.SDK page as an image fill, with control over size, position, colors, and metadata for later updates. ![A QR code rendered as an image fill on a page.](./assets/swift-based.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-shapes-qrcode) QR codes are a practical way to turn any design into a scannable gateway for landing pages, app installs, product info, or event tickets. CE.SDK does not ship a built-in QR generator, but Core Image includes one — encode the URL, colorize it, and hand the resulting image to a graphic block through an image fill. ## Platform Setup The example defines `PlatformColor` and `PlatformImage` type aliases so the QR generation code compiles unchanged across every Apple target. ```swift highlight-qr-imports import CoreImage.CIFilterBuiltins import Foundation import IMGLYEngine #if canImport(UIKit) import UIKit private typealias PlatformColor = UIColor private typealias PlatformImage = UIImage #elseif canImport(AppKit) import AppKit private typealias PlatformColor = NSColor private typealias PlatformImage = NSImage #endif ``` ## Generate a QR Code Image Use Core Image's `CIFilter.qrCodeGenerator()` to encode the string, then run the output through `CIFilter.falseColor()` to remap the default black-and-white pixels onto the requested foreground and background colors. Scale without interpolation so the QR modules stay sharp when the block is rendered or exported. ```swift highlight-qr-generate /// Generate a QR code image from a string using Core Image. /// - Parameters: /// - string: Content to encode; use a full URL with scheme. /// - correction: Error correction level (L, M, Q, H). "M" is a good default. /// - scale: Pixel scale factor. Increase for print. /// - foreground: Dark module color. /// - background: Light background color. private func makeQRCode( from string: String, correction: String = "M", scale: CGFloat = 10, foreground: PlatformColor = .black, background: PlatformColor = .white, ) throws -> PlatformImage { guard let data = string.data(using: .utf8) else { throw QRGenerationError.invalidInput } let qr = CIFilter.qrCodeGenerator() qr.setValue(data, forKey: "inputMessage") qr.setValue(correction, forKey: "inputCorrectionLevel") guard let output = qr.outputImage else { throw QRGenerationError.filterFailed } // Map the filter's default black-and-white output to the requested colors. let falseColor = CIFilter.falseColor() falseColor.inputImage = output #if canImport(UIKit) falseColor.color0 = CIColor(color: foreground) falseColor.color1 = CIColor(color: background) #elseif canImport(AppKit) falseColor.color0 = CIColor(color: foreground) ?? CIColor.black falseColor.color1 = CIColor(color: background) ?? CIColor.white #endif guard let colored = falseColor.outputImage else { throw QRGenerationError.filterFailed } // Scale without interpolation so QR modules stay crisp. let scaled = colored.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) let context = CIContext(options: [.useSoftwareRenderer: false]) guard let cg = context.createCGImage(scaled, from: scaled.extent) else { throw QRGenerationError.filterFailed } #if canImport(UIKit) return UIImage(cgImage: cg, scale: 1.0, orientation: .up) #elseif canImport(AppKit) return NSImage(cgImage: cg, size: NSSize(width: cg.width, height: cg.height)) #endif } private enum QRGenerationError: Error { case invalidInput case filterFailed case encodingFailed } ``` Keep the foreground dark and the background light for reliable scanning. The `correction` argument selects the error-correction level (L, M, Q, H) — higher levels tolerate more damage at the cost of encoded capacity. ## Insert the QR as an Image Fill The engine's image fill reads pixel data from a file URL, so encode the QR image to PNG bytes, write them to a temporary file, and point the fill at that file. Then create a graphic block with a `rect` shape, apply the image fill, and set square dimensions so the modules do not distort. ```swift highlight-qr-insert let qrImage = try makeQRCode(from: "https://img.ly") // Both PNG-encoding branches share the same output format. #if canImport(UIKit) guard let png = qrImage.pngData() else { throw QRGenerationError.encodingFailed } #elseif canImport(AppKit) guard let tiff = qrImage.tiffRepresentation, let bitmap = NSBitmapImageRep(data: tiff), let png = bitmap.representation(using: .png, properties: [:]) else { throw QRGenerationError.encodingFailed } #endif let qrFileURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("png") try png.write(to: qrFileURL) let qrBlock = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) try engine.block.setShape(qrBlock, shape: rectShape) let imageFill = try engine.block.createFill(.image) try engine.block.setURL(imageFill, property: "fill/image/imageFileURI", value: qrFileURL) try engine.block.setFill(qrBlock, fill: imageFill) try engine.block.setWidth(qrBlock, value: 300) try engine.block.setHeight(qrBlock, value: 300) try engine.block.setPositionX(qrBlock, value: 250) try engine.block.setPositionY(qrBlock, value: 150) try engine.block.appendChild(to: page, child: qrBlock) ``` The snippet keeps `width` and `height` equal. Stretching a QR code prevents scanners from decoding it. ## Add Optional Metadata Store the encoded URL alongside the block so a future update can regenerate the image without decoding it back out of the pixels. Metadata `key` values are free-form, and both the key and value must be `String`. ```swift highlight-qr-metadata try engine.block.setMetadata(qrBlock, key: "qr/url", value: "https://img.ly") ``` ## Update an Existing QR Code When the encoded URL changes, generate a new QR image, write it to a fresh temporary file, and repoint the block's fill URI at the new file. Read the block's fill with `getFill(_:)` — the URI property lives on the fill, not the graphic block. ```swift highlight-qr-update let updatedURL = "https://img.ly/showcases" let updatedImage = try makeQRCode(from: updatedURL) #if canImport(UIKit) guard let updatedPng = updatedImage.pngData() else { throw QRGenerationError.encodingFailed } #elseif canImport(AppKit) guard let updatedTiff = updatedImage.tiffRepresentation, let updatedBitmap = NSBitmapImageRep(data: updatedTiff), let updatedPng = updatedBitmap.representation(using: .png, properties: [:]) else { throw QRGenerationError.encodingFailed } #endif let updatedFileURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("png") try updatedPng.write(to: updatedFileURL) let fill = try engine.block.getFill(qrBlock) try engine.block.setURL(fill, property: "fill/image/imageFileURI", value: updatedFileURL) try engine.block.setMetadata(qrBlock, key: "qr/url", value: updatedURL) ``` To generate many QR codes in a batch, loop through your data and call the same insert flow once per URL. ## Troubleshooting | Symptom | Cause | Solution | |---------|-------|----------| | QR looks blurry | Image scaled too small | Increase the Core Image `scale` and the block dimensions. | | QR won't scan | Low contrast or invalid URL | Use dark-on-light colors and percent-encode URLs. | | QR not visible | Shape missing from block | Call `setShape` before applying the fill. | | App crash writing file | Invalid temp URL | Always use `FileManager.default.temporaryDirectory`. | ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.scene.create()` | Create a new scene to host the QR code. | | `engine.block.create(_:)` | Create a graphic or page block. | | `engine.block.createShape(_:)` | Create a rectangle shape for the QR code. | | `engine.block.setShape(_:shape:)` | Apply the shape to the graphic block. | | `engine.block.createFill(_:)` | Create an image fill for the QR code. | | `engine.block.setURL(_:property:value:)` | Point the fill at the QR PNG file URL. | | `engine.block.setFill(_:fill:)` | Apply the fill to the graphic block. | | `engine.block.getFill(_:)` | Read the current fill when updating the QR image. | | `engine.block.setWidth(_:value:)` | Set the QR code width. | | `engine.block.setHeight(_:value:)` | Set the QR code height (keep equal to width). | | `engine.block.setPositionX(_:value:)` | Set the horizontal position. | | `engine.block.setPositionY(_:value:)` | Set the vertical position. | | `engine.block.appendChild(to:child:)` | Add the QR block to the page. | | `engine.block.setMetadata(_:key:value:)` | Store the encoded URL on the block for later updates. | ### Properties | Property | Type | Description | | --- | --- | --- | | `fill/image/imageFileURI` | URL | File URL of the QR PNG on the image fill. | ## Next Steps Now that you can generate QR codes, here are some related guides. - [Insert Shapes or Stickers](https://img.ly/docs/cesdk/mac-catalyst/insert-media/shapes-or-stickers-20ac68/) — Learn how fills and shapes interact. - [Batch Processing](https://img.ly/docs/cesdk/mac-catalyst/automation/batch-processing-ab2d18/) — Automate multiple QR insertions. - [Export to PDF](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/export/to-pdf-95e04b/) — Prepare print-ready designs. - [Use Templates: Overview](https://img.ly/docs/cesdk/mac-catalyst/create-templates/overview-4ebe30/) — Add a placeholder for QR blocks in 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: "Text" description: "Add, style, and customize text layers in your design using CE.SDK’s flexible text editing tools." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) --- --- ## Related Pages - [Overview](https://img.ly/docs/cesdk/mac-catalyst/text/overview-0bd620/) - Add, style, and customize text layers in your design using CE.SDK’s flexible text editing tools. - [Add Text](https://img.ly/docs/cesdk/mac-catalyst/text/add-4f5011/) - Create and configure text blocks with custom fonts, rich text styling, and dynamic sizing options. - [Edit Text](https://img.ly/docs/cesdk/mac-catalyst/text/edit-c5106b/) - Edit text content programmatically with range-based APIs for replacing, formatting, and querying text. - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) - Apply fonts, colors, alignment, and other styling options to customize text appearance. - [Text Decorations](https://img.ly/docs/cesdk/mac-catalyst/text/decorations-d3c0a1/) - Add underline, strikethrough, and overline decorations to text with customizable styles, colors, and thickness. - [Text on a Path](https://img.ly/docs/cesdk/mac-catalyst/text/text-on-path-e3b8a2/) - Place text along an SVG path — a circle, arch, wave, or any curve — with the setTextOnPath engine API or, on iOS, the editor's built-in Path inspector. - [Text Designs](https://img.ly/docs/cesdk/mac-catalyst/text/text-designs-a1b2c3/) - Create and customize text component libraries using predefined text designs that appear in your asset library. - [Text Enumerations](https://img.ly/docs/cesdk/mac-catalyst/text/enumerations-b5c1d2/) - Add bullet lists and numbered lists to text blocks in CE.SDK using per-paragraph list styles and nesting levels. - [Auto-Size](https://img.ly/docs/cesdk/mac-catalyst/text/auto-size-5331b3/) - Configure text blocks to automatically adapt their dimensions or font size for dynamic content. - [Text Effects](https://img.ly/docs/cesdk/mac-catalyst/text/effects-2dc9fc/) - Add visual depth and interest to text blocks using drop shadows and stroke outlines. - [Emojis](https://img.ly/docs/cesdk/mac-catalyst/text/emojis-510651/) - Configure emoji rendering in CE.SDK text blocks with a dedicated emoji font for consistent display across platforms. - [Adjust Text Spacing](https://img.ly/docs/cesdk/mac-catalyst/text/adjust-spacing-c1a3b6/) - Control letter spacing, line height, paragraph line height, and paragraph spacing in text blocks. - [Text and Language Support](https://img.ly/docs/cesdk/mac-catalyst/text/language-support-a0f010/) - Create designs that work across different languages and writing systems with RTL text, complex scripts, and multilingual font support. --- ## 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 Text" description: "Create and configure text blocks with custom fonts, rich text styling, and dynamic sizing options." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/add-4f5011/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Add Text](https://img.ly/docs/cesdk/mac-catalyst/text/add-4f5011/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-add/AddText.swift reference-only import Foundation import IMGLYEngine @MainActor func addText(engine: Engine) async throws { // Demo scaffolding: a Pixel-unit page so `text/fontSize` literals interpret // as pixels and the captured exports show the rendered output at its // intended scale. 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: 960) try engine.block.appendChild(to: scene, child: page) let titleBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: titleBlock) try engine.block.replaceText(titleBlock, text: "Welcome to CE.SDK") try engine.block.setWidthMode(titleBlock, mode: .auto) try engine.block.setHeightMode(titleBlock, mode: .auto) try engine.block.setPositionX(titleBlock, value: 40) try engine.block.setPositionY(titleBlock, value: 40) // The example builds font file URLs from a base URL that points at the // CE.SDK asset location. Replace it with wherever your app bundles or hosts // the CE.SDK font assets. let baseURL = try engine.guidesBaseURL let caveatRegular = Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Caveat/Caveat-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ) let caveatBold = Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Caveat/Caveat-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ) let caveatTypeface = Typeface(name: "Caveat", fonts: [caveatRegular, caveatBold]) try engine.block.setFont(titleBlock, fontFileURL: caveatBold.uri, typeface: caveatTypeface) try engine.block.setTextFontSize(titleBlock, fontSize: 48) let robotoRegular = Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ) let robotoTypeface = Typeface( name: "Roboto", fonts: [ robotoRegular, Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Italic.ttf"), subFamily: "Italic", weight: .normal, style: .italic, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-BoldItalic.ttf"), subFamily: "Bold Italic", weight: .bold, style: .italic, ), ], ) // Scaffolding: a second text block, fixed to the page width so the range // styling below wraps within the page. The styling snippet operates on it. let richText = "Rich text with colors and styles" let richTextBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: richTextBlock) try engine.block.replaceText(richTextBlock, text: richText) try engine.block.setPositionX(richTextBlock, value: 40) try engine.block.setPositionY(richTextBlock, value: 140) try engine.block.setWidth(richTextBlock, value: 720) try engine.block.setWidthMode(richTextBlock, mode: .absolute) try engine.block.setHeightMode(richTextBlock, mode: .auto) try engine.block.setTextFontSize(richTextBlock, fontSize: 48) try engine.block.setFont(richTextBlock, fontFileURL: robotoRegular.uri, typeface: robotoTypeface) // "Rich" in blue. try engine.block.setTextColor(richTextBlock, color: .rgba(r: 0.2, g: 0.4, b: 0.8), in: richText.range(of: "Rich")!) // "text" in bold. try engine.block.setTextFontWeight(richTextBlock, fontWeight: .bold, in: richText.range(of: "text")!) // "with" in italic. try engine.block.setTextFontStyle(richTextBlock, fontStyle: .italic, in: richText.range(of: "with")!) // "colors" in orange and larger type. try engine.block.setTextColor(richTextBlock, color: .rgba(r: 0.9, g: 0.5, b: 0.1), in: richText.range(of: "colors")!) try engine.block.setTextFontSize(richTextBlock, fontSize: 56, in: richText.range(of: "colors")!) // "and" uses a different typeface. try engine.block.setTypeface(richTextBlock, typeface: caveatTypeface, in: richText.range(of: "and")!) // "styles" in green uppercase. try engine.block.setTextColor(richTextBlock, color: .rgba(r: 0.2, g: 0.7, b: 0.3), in: richText.range(of: "styles")!) try engine.block.setTextCase(richTextBlock, textCase: .uppercase, in: richText.range(of: "styles")!) try await engine.captureGuide(page, label: "after-rich-text") let autoSizeBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: autoSizeBlock) try engine.block.replaceText(autoSizeBlock, text: "Auto-sizing text block") try engine.block.setPositionX(autoSizeBlock, value: 40) try engine.block.setPositionY(autoSizeBlock, value: 300) try engine.block.setWidth(autoSizeBlock, value: 720) try engine.block.setWidthMode(autoSizeBlock, mode: .absolute) try engine.block.setHeightMode(autoSizeBlock, mode: .auto) try engine.block.setTextFontSize(autoSizeBlock, fontSize: 48) let caseBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: caseBlock) try engine.block.replaceText(caseBlock, text: "uppercase text") try engine.block.setPositionX(caseBlock, value: 40) try engine.block.setPositionY(caseBlock, value: 420) try engine.block.setWidthMode(caseBlock, mode: .auto) try engine.block.setHeightMode(caseBlock, mode: .auto) try engine.block.setTextFontSize(caseBlock, fontSize: 48) try engine.block.setTextCase(caseBlock, textCase: .uppercase) let alignedBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: alignedBlock) try engine.block.replaceText(alignedBlock, text: "Centered Text\nWith Line Spacing") try engine.block.setPositionX(alignedBlock, value: 40) try engine.block.setPositionY(alignedBlock, value: 540) try engine.block.setWidth(alignedBlock, value: 720) try engine.block.setWidthMode(alignedBlock, mode: .absolute) try engine.block.setHeightMode(alignedBlock, mode: .auto) try engine.block.setTextFontSize(alignedBlock, fontSize: 48) try engine.block.setTextHorizontalAlignment(alignedBlock, alignment: .center) try engine.block.setTextLineHeight(alignedBlock, lineHeight: 1.5) try engine.block.setFloat(alignedBlock, property: "text/letterSpacing", value: 0.05) // Scaffolding: a block styled with all four Roboto variants so both toggles // have a matching counterpart variant to switch to. let toggleText = "Toggle Bold and Italic" let toggleBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: toggleBlock) try engine.block.replaceText(toggleBlock, text: toggleText) try engine.block.setPositionX(toggleBlock, value: 40) try engine.block.setPositionY(toggleBlock, value: 740) try engine.block.setWidthMode(toggleBlock, mode: .auto) try engine.block.setHeightMode(toggleBlock, mode: .auto) try engine.block.setTextFontSize(toggleBlock, fontSize: 48) try engine.block.setFont(toggleBlock, fontFileURL: robotoRegular.uri, typeface: robotoTypeface) let boldRange = toggleText.range(of: "Bold")! if try engine.block.canToggleBoldFont(toggleBlock, in: boldRange) { try engine.block.toggleBoldFont(toggleBlock, in: boldRange) } let italicRange = toggleText.range(of: "Italic")! if try engine.block.canToggleItalicFont(toggleBlock, in: italicRange) { try engine.block.toggleItalicFont(toggleBlock, in: italicRange) } let helloWorld = "Hello World" let modifyBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: modifyBlock) try engine.block.replaceText(modifyBlock, text: helloWorld) try engine.block.setPositionX(modifyBlock, value: 40) try engine.block.setPositionY(modifyBlock, value: 840) try engine.block.setWidthMode(modifyBlock, mode: .auto) try engine.block.setHeightMode(modifyBlock, mode: .auto) try engine.block.setTextFontSize(modifyBlock, fontSize: 48) // Replace "World" while keeping the surrounding text. try engine.block.replaceText(modifyBlock, text: "CE.SDK", in: helloWorld.range(of: "World")!) // Remove the leading "Hello " to leave just "CE.SDK". try engine.block.removeText(modifyBlock, from: "Hello CE.SDK".range(of: "Hello ")!) try await engine.captureGuide(page, label: "hero") } ``` Create and configure text blocks in CE.SDK with custom fonts, rich text styling, and dynamic sizing options. ![Rendered text scene with styled text blocks](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-text-add) Text blocks are fundamental design elements for displaying titles, captions, labels, and body text. CE.SDK provides range-based styling APIs so a single text block can contain multiple colors, font weights, font styles, and text cases. The snippets below assume you already have an `Engine` instance and the target page block you want to add text to. ## Create Text Blocks Create a text block with `engine.block.create(.text)` and attach it to a page with `appendChild(to:child:)`. Use `replaceText(_:text:)` for both the initial text and later text replacements. ```swift highlight-addText-create let titleBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: titleBlock) try engine.block.replaceText(titleBlock, text: "Welcome to CE.SDK") try engine.block.setWidthMode(titleBlock, mode: .auto) try engine.block.setHeightMode(titleBlock, mode: .auto) try engine.block.setPositionX(titleBlock, value: 40) try engine.block.setPositionY(titleBlock, value: 40) ``` Text blocks can size themselves to their content. Set the width and height modes to `.auto` when the block should fit its text. ## Apply Fonts and Typefaces Apply a font with `setFont(_:fontFileURL:typeface:)`. Provide both the concrete font file URL and a `Typeface` that describes the font family and its available variants. ```swift highlight-addText-set-font let caveatRegular = Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Caveat/Caveat-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ) let caveatBold = Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Caveat/Caveat-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ) let caveatTypeface = Typeface(name: "Caveat", fonts: [caveatRegular, caveatBold]) try engine.block.setFont(titleBlock, fontFileURL: caveatBold.uri, typeface: caveatTypeface) try engine.block.setTextFontSize(titleBlock, fontSize: 48) ``` Use `setFont` to replace the block's active font file. Use `setTypeface(_:typeface:in:)` when you want CE.SDK to keep the current formatting — such as bold or italic — as far as the new typeface supports it. The example builds font file URLs from a base URL that points at the CE.SDK asset location. Replace it with wherever your app bundles or hosts the CE.SDK font assets. When later snippets apply weight, style, or toggle formatting, use a typeface that includes the variants those operations need. ```swift highlight-addText-font-variants let robotoRegular = Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ) let robotoTypeface = Typeface( name: "Roboto", fonts: [ robotoRegular, Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Italic.ttf"), subFamily: "Italic", weight: .normal, style: .italic, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-BoldItalic.ttf"), subFamily: "Bold Italic", weight: .bold, style: .italic, ), ], ) ``` ## Style Text Ranges Style part of a text block by passing a range to the range-based text APIs. Swift ranges are `Range` values built from the text string with `range(of:)`; omitting the range targets the whole block. ```swift highlight-addText-rich-text-styling // "Rich" in blue. try engine.block.setTextColor(richTextBlock, color: .rgba(r: 0.2, g: 0.4, b: 0.8), in: richText.range(of: "Rich")!) // "text" in bold. try engine.block.setTextFontWeight(richTextBlock, fontWeight: .bold, in: richText.range(of: "text")!) // "with" in italic. try engine.block.setTextFontStyle(richTextBlock, fontStyle: .italic, in: richText.range(of: "with")!) // "colors" in orange and larger type. try engine.block.setTextColor(richTextBlock, color: .rgba(r: 0.9, g: 0.5, b: 0.1), in: richText.range(of: "colors")!) try engine.block.setTextFontSize(richTextBlock, fontSize: 56, in: richText.range(of: "colors")!) // "and" uses a different typeface. try engine.block.setTypeface(richTextBlock, typeface: caveatTypeface, in: richText.range(of: "and")!) // "styles" in green uppercase. try engine.block.setTextColor(richTextBlock, color: .rgba(r: 0.2, g: 0.7, b: 0.3), in: richText.range(of: "styles")!) try engine.block.setTextCase(richTextBlock, textCase: .uppercase, in: richText.range(of: "styles")!) ``` The same range pattern applies to text colors, font weights, font styles, font sizes, typefaces, and text cases. ## Configure Auto-Sizing Combine a fixed width with an automatic height when text should wrap and grow vertically with its content. ```swift highlight-addText-auto-sizing let autoSizeBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: autoSizeBlock) try engine.block.replaceText(autoSizeBlock, text: "Auto-sizing text block") try engine.block.setPositionX(autoSizeBlock, value: 40) try engine.block.setPositionY(autoSizeBlock, value: 300) try engine.block.setWidth(autoSizeBlock, value: 720) try engine.block.setWidthMode(autoSizeBlock, mode: .absolute) try engine.block.setHeightMode(autoSizeBlock, mode: .auto) try engine.block.setTextFontSize(autoSizeBlock, fontSize: 48) ``` Use `.absolute` when a dimension should use an explicit design-unit value, `.percent` when it should follow the parent size, and `.auto` when CE.SDK should derive it from the content. ## Apply Text Case Transformations Use `setTextCase(_:textCase:)` to change the rendered casing without changing the underlying text string. ```swift highlight-addText-text-case let caseBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: caseBlock) try engine.block.replaceText(caseBlock, text: "uppercase text") try engine.block.setPositionX(caseBlock, value: 40) try engine.block.setPositionY(caseBlock, value: 420) try engine.block.setWidthMode(caseBlock, mode: .auto) try engine.block.setHeightMode(caseBlock, mode: .auto) try engine.block.setTextFontSize(caseBlock, fontSize: 48) try engine.block.setTextCase(caseBlock, textCase: .uppercase) ``` Available text cases are `.normal`, `.uppercase`, `.lowercase`, and `.titlecase`. ## Set Text Alignment and Spacing Set paragraph alignment with `setTextHorizontalAlignment(_:alignment:)`, line height with `setTextLineHeight(_:lineHeight:)`, and letter spacing with the `text/letterSpacing` float property. ```swift highlight-addText-text-alignment let alignedBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: alignedBlock) try engine.block.replaceText(alignedBlock, text: "Centered Text\nWith Line Spacing") try engine.block.setPositionX(alignedBlock, value: 40) try engine.block.setPositionY(alignedBlock, value: 540) try engine.block.setWidth(alignedBlock, value: 720) try engine.block.setWidthMode(alignedBlock, mode: .absolute) try engine.block.setHeightMode(alignedBlock, mode: .auto) try engine.block.setTextFontSize(alignedBlock, fontSize: 48) try engine.block.setTextHorizontalAlignment(alignedBlock, alignment: .center) try engine.block.setTextLineHeight(alignedBlock, lineHeight: 1.5) try engine.block.setFloat(alignedBlock, property: "text/letterSpacing", value: 0.05) ``` Line height is a multiplier of the font size, so `1.5` means 150%. Letter spacing is a proportion of the font size. ## Toggle Bold and Italic Use the toggle helpers when you want CE.SDK to switch between the normal and formatted variants. Check support first, because each toggle needs a matching counterpart variant for the current range and style. Include normal, bold, italic, and bold italic variants when both toggles should work across all four states. ```swift highlight-addText-toggle-bold-italic let boldRange = toggleText.range(of: "Bold")! if try engine.block.canToggleBoldFont(toggleBlock, in: boldRange) { try engine.block.toggleBoldFont(toggleBlock, in: boldRange) } let italicRange = toggleText.range(of: "Italic")! if try engine.block.canToggleItalicFont(toggleBlock, in: italicRange) { try engine.block.toggleItalicFont(toggleBlock, in: italicRange) } ``` The range argument targets the same text ranges used by the styling APIs. ## Modify Text Content Use `replaceText(_:text:in:)` with a range to replace part of the text while keeping the surrounding content. Omitting the range replaces the full text string. ```swift highlight-addText-modify-text let helloWorld = "Hello World" let modifyBlock = try engine.block.create(.text) try engine.block.appendChild(to: page, child: modifyBlock) try engine.block.replaceText(modifyBlock, text: helloWorld) try engine.block.setPositionX(modifyBlock, value: 40) try engine.block.setPositionY(modifyBlock, value: 840) try engine.block.setWidthMode(modifyBlock, mode: .auto) try engine.block.setHeightMode(modifyBlock, mode: .auto) try engine.block.setTextFontSize(modifyBlock, fontSize: 48) // Replace "World" while keeping the surrounding text. try engine.block.replaceText(modifyBlock, text: "CE.SDK", in: helloWorld.range(of: "World")!) // Remove the leading "Hello " to leave just "CE.SDK". try engine.block.removeText(modifyBlock, from: "Hello CE.SDK".range(of: "Hello ")!) ``` Use `removeText(_:from:)` with the same range convention when you need to delete text from a block. ## Troubleshooting ### Text Not Displaying - Verify that `replaceText` set the text content. - Check that `appendChild(to:child:)` attaches the text block to a page. - Ensure the text block has a width or uses automatic sizing. ### Font Not Loading - Verify the font URL is reachable from the device. - Check that the `Typeface` metadata matches the font files. - Include every font variant needed by the bold and italic toggles. ### Range Styling Not Applying - Verify that the range is valid and non-empty for the current text. - Check that the active typeface supports the requested font weight or style. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.create(.text)` | Create a new text block. | | `engine.block.appendChild(to:child:)` | Attach the text block to a page or scene hierarchy. | | `engine.block.replaceText(_:text:)` | Set or replace the full text content. | | `engine.block.replaceText(_:text:in:)` | Replace the text within a range. | | `engine.block.removeText(_:from:)` | Remove the text within a range. | | `engine.block.setFont(_:fontFileURL:typeface:)` | Bind a concrete font file and typeface to a text block. | | `engine.block.setTypeface(_:typeface:in:)` | Apply a typeface while preserving compatible formatting. | | `engine.block.setTextColor(_:color:in:)` | Apply text color to a range. | | `engine.block.setTextFontWeight(_:fontWeight:in:)` | Apply font weight to a range. | | `engine.block.setTextFontStyle(_:fontStyle:in:)` | Apply font style to a range. | | `engine.block.setTextFontSize(_:fontSize:in:)` | Apply font size to the block or a range. | | `engine.block.setTextCase(_:textCase:in:)` | Apply a visual text case transformation to the block or a range. | | `engine.block.canToggleBoldFont(_:in:)` | Check whether a range can switch to or from bold. | | `engine.block.toggleBoldFont(_:in:)` | Toggle bold styling for a range. | | `engine.block.canToggleItalicFont(_:in:)` | Check whether a range can switch to or from italic. | | `engine.block.toggleItalicFont(_:in:)` | Toggle italic styling for a range. | | `engine.block.setWidthMode(_:mode:)` | Set how CE.SDK resolves a block width. | | `engine.block.setHeightMode(_:mode:)` | Set how CE.SDK resolves a block height. | | `engine.block.setWidth(_:value:)` | Set an explicit block width. | | `engine.block.setPositionX(_:value:)` | Set a block's horizontal position. | | `engine.block.setPositionY(_:value:)` | Set a block's vertical position. | | `engine.block.setTextHorizontalAlignment(_:alignment:)` | Set horizontal alignment for the block. | | `engine.block.setTextLineHeight(_:lineHeight:)` | Set line height for all paragraphs. | | `engine.block.setFloat(_:property:value:)` | Set a float property, such as letter spacing. | ### Properties | Property | Type | Description | | --- | --- | --- | | `text/letterSpacing` | Float | Letter spacing as a proportion of the font size. | ## Next Steps - [Style Text](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) - Apply fills, strokes, and backgrounds to text. - [Auto-Size Text](https://img.ly/docs/cesdk/mac-catalyst/text/auto-size-5331b3/) - Configure text blocks to resize dynamically. - [Adjust Text Spacing](https://img.ly/docs/cesdk/mac-catalyst/text/adjust-spacing-c1a3b6/) - Fine-tune letter and line spacing. - [Add Emojis](https://img.ly/docs/cesdk/mac-catalyst/text/emojis-510651/) - Display emoji characters with custom fonts. - [Add Text Effects](https://img.ly/docs/cesdk/mac-catalyst/text/effects-2dc9fc/) - Apply visual effects to text 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: "Adjust Text Spacing" description: "Control letter spacing, line height, paragraph line height, and paragraph spacing in text blocks." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/adjust-spacing-c1a3b6/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Adjust Spacing](https://img.ly/docs/cesdk/mac-catalyst/text/adjust-spacing-c1a3b6/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-adjust-spacing/TextAdjustSpacing.swift reference-only import Foundation import IMGLYEngine @MainActor func textAdjustSpacing(engine: Engine) async throws { // Demo scaffolding: a Pixel-unit page with a styled multi-paragraph text block // so each spacing change is visibly demonstrable in the captured exports. 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: 800) try engine.block.appendChild(to: scene, child: page) let text = try engine.block.create(.text) try engine.block.appendChild(to: page, child: text) try engine.block.setWidthMode(text, mode: .auto) try engine.block.setHeightMode(text, mode: .auto) try engine.block.replaceText(text, text: "Hello\nWorld\nCE.SDK") try engine.block.setFloat(text, property: "text/fontSize", value: 60) try engine.block.setPositionX(text, value: 200) try engine.block.setPositionY(text, value: 100) try engine.block.setFloat(text, property: "text/letterSpacing", value: 0.1) let letterSpacing = try engine.block.getFloat(text, property: "text/letterSpacing") print("Letter spacing: \(letterSpacing)") try await engine.captureGuide(page, label: "after-letter-spacing") try engine.block.setFloat(text, property: "text/lineHeight", value: 1.5) let lineHeight = try engine.block.getFloat(text, property: "text/lineHeight") print("Block-level line height: \(lineHeight)") try await engine.captureGuide(page, label: "after-line-height") // Override paragraph 0; paragraph 1 still reads the block-level value. try engine.block.setTextLineHeight(text, lineHeight: 2.0, paragraphIndex: 0) let paragraph0LineHeight = try engine.block.getTextLineHeight(text, paragraphIndex: 0) let paragraph1LineHeight = try engine.block.getTextLineHeight(text, paragraphIndex: 1) print("Paragraph 0: \(paragraph0LineHeight)") print("Paragraph 1: \(paragraph1LineHeight)") // Pass nil for lineHeight to clear the override; paragraph 0 reverts to the block-level value. try engine.block.setTextLineHeight(text, lineHeight: nil, paragraphIndex: 0) let clearedParagraph0LineHeight = try engine.block.getTextLineHeight(text, paragraphIndex: 0) print("Paragraph 0 after clearing: \(clearedParagraph0LineHeight)") // Omit paragraphIndex to update the block-level value and clear every paragraph override. try engine.block.setTextLineHeight(text, lineHeight: 1.8) let resetBlockLineHeight = try engine.block.getTextLineHeight(text, paragraphIndex: 1) print("Block-level after reset: \(resetBlockLineHeight)") try engine.block.setFloat(text, property: "text/paragraphSpacing", value: 1.2) let paragraphSpacing = try engine.block.getFloat(text, property: "text/paragraphSpacing") print("Paragraph spacing: \(paragraphSpacing)") try await engine.captureGuide(page, label: "hero") } ``` Control letter spacing, line height, and paragraph spacing in text blocks using the Block API. ![Three-paragraph text block with adjusted letter spacing, line height, and paragraph spacing.](./assets/swift-based.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-text-adjust-spacing) Three text spacing properties — `text/letterSpacing`, `text/lineHeight`, and `text/paragraphSpacing` — control the spacing between characters, lines, and paragraphs in a text block. The Block API reads and writes the block-level values; line height additionally supports per-paragraph overrides. The sample operates on a text block with multiple characters, lines, and paragraph breaks so each spacing change is observable. ## Letter Spacing Control the horizontal space between characters with the `text/letterSpacing` property. Positive values spread characters apart; negative values tighten them. The accepted range is `-0.15` to `1.4`. ```swift highlight-letter-spacing try engine.block.setFloat(text, property: "text/letterSpacing", value: 0.1) let letterSpacing = try engine.block.getFloat(text, property: "text/letterSpacing") print("Letter spacing: \(letterSpacing)") ``` Letter spacing, also called tracking, adjusts the density of a text block without changing the text content. ## Line Height Control the vertical distance between lines with the `text/lineHeight` property. The value is a multiplier of the font size, so `1.5` renders lines at 150% of the font size. The accepted range is `0.5` to `2.5`. ```swift highlight-line-height try engine.block.setFloat(text, property: "text/lineHeight", value: 1.5) let lineHeight = try engine.block.getFloat(text, property: "text/lineHeight") print("Block-level line height: \(lineHeight)") ``` This block-level value applies to every paragraph unless a paragraph has its own override. ## Per-Paragraph Line Height Each paragraph can override the block-level line height. Use `setTextLineHeight` to apply or clear an override, and `getTextLineHeight` to read the effective value for any paragraph — the override if one is set, otherwise the block-level fallback. ```swift highlight-paragraph-line-height // Override paragraph 0; paragraph 1 still reads the block-level value. try engine.block.setTextLineHeight(text, lineHeight: 2.0, paragraphIndex: 0) let paragraph0LineHeight = try engine.block.getTextLineHeight(text, paragraphIndex: 0) let paragraph1LineHeight = try engine.block.getTextLineHeight(text, paragraphIndex: 1) print("Paragraph 0: \(paragraph0LineHeight)") print("Paragraph 1: \(paragraph1LineHeight)") // Pass nil for lineHeight to clear the override; paragraph 0 reverts to the block-level value. try engine.block.setTextLineHeight(text, lineHeight: nil, paragraphIndex: 0) let clearedParagraph0LineHeight = try engine.block.getTextLineHeight(text, paragraphIndex: 0) print("Paragraph 0 after clearing: \(clearedParagraph0LineHeight)") // Omit paragraphIndex to update the block-level value and clear every paragraph override. try engine.block.setTextLineHeight(text, lineHeight: 1.8) let resetBlockLineHeight = try engine.block.getTextLineHeight(text, paragraphIndex: 1) print("Block-level after reset: \(resetBlockLineHeight)") ``` ## Paragraph Spacing Add vertical space after paragraph breaks with the `text/paragraphSpacing` property. The value is an EM-based gap relative to the text's font size, not an absolute pixel distance, and it only affects text that contains newline characters. The accepted range is `0.0` to `2.5`. ```swift highlight-paragraph-spacing try engine.block.setFloat(text, property: "text/paragraphSpacing", value: 1.2) let paragraphSpacing = try engine.block.getFloat(text, property: "text/paragraphSpacing") print("Paragraph spacing: \(paragraphSpacing)") ``` Single-paragraph text shows no visible difference because there is no paragraph break to separate. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.setFloat(_:property:value:)` | Set a block-level spacing property (letter spacing, line height, or paragraph spacing). | | `engine.block.getFloat(_:property:)` | Read the current value of a block-level spacing property. | | `engine.block.setTextLineHeight(_:lineHeight:paragraphIndex:)` | Set or clear a paragraph-specific line-height override. | | `engine.block.setTextLineHeight(_:lineHeight:)` | Set the block-level line height and clear all paragraph overrides. | | `engine.block.getTextLineHeight(_:paragraphIndex:)` | Read the effective line height for a paragraph. | ### Properties | Property | Type | Description | | --- | --- | --- | | `text/letterSpacing` | `Float` | Space between characters (`-0.15` to `1.4`). | | `text/lineHeight` | `Float` | Block-level multiplier for vertical line distance (`0.5` to `2.5`). | | `text/paragraphSpacing` | `Float` | EM-based gap added after paragraph breaks (`0.0` to `2.5`). | ## Troubleshooting **Spacing changes are not visible.** Check that the text block contains content that exercises the property: multiple characters for letter spacing, multiple lines for line height, and paragraph breaks for paragraph spacing. **Line height looks larger than expected.** Line height is a multiplier, not an absolute pixel value. A value of `1.5` means 150% of the current font size. **Paragraph spacing has no effect.** Verify that the text contains newline characters. Paragraph spacing is only visible between paragraphs. The value is relative to the text's font size (typically `0.0` to `2.5`), not the `text/lineHeight` multiplier or a pixel distance. ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) — Apply fonts, colors, alignment, and other styling options to customize text appearance. - [Add Text](https://img.ly/docs/cesdk/mac-catalyst/text/add-4f5011/) — Insert text blocks into your CE.SDK scene. - [Auto-Size](https://img.ly/docs/cesdk/mac-catalyst/text/auto-size-5331b3/) — Configure text blocks to automatically adapt their dimensions or font size for dynamic content. --- ## 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-Size" description: "Configure text blocks to automatically adapt their dimensions or font size for dynamic content." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/auto-size-5331b3/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Auto-Size](https://img.ly/docs/cesdk/mac-catalyst/text/auto-size-5331b3/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-auto-size/TextAutoSize.swift reference-only import Foundation import IMGLYEngine @MainActor func textAutoSize(engine: Engine) async throws { // Demo scaffolding: a page with a cream background frames the text blocks below. // The Pixel design unit pairs the font-size unit to Pixel, so `setTextFontSize` // values are interpreted as pixels — matching the dimensions and positions below. 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 background = try engine.block.create(.graphic) try engine.block.setShape(background, shape: engine.block.createShape(.rect)) try engine.block.setWidth(background, value: 800) try engine.block.setHeight(background, value: 600) try engine.block.setFill(background, fill: engine.block.createFill(.color)) let backgroundFill = try engine.block.getFill(background) try engine.block.setColor( backgroundFill, property: "fill/color/value", color: .rgba(r: 0.969, g: 0.957, b: 0.937, a: 1.0), ) try engine.block.appendChild(to: page, child: background) let autoText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: autoText) try engine.block.setWidthMode(autoText, mode: .auto) try engine.block.setHeightMode(autoText, mode: .auto) try engine.block.replaceText(autoText, text: "Auto-sized text") try engine.block.setTextFontSize(autoText, fontSize: 36) try engine.block.setTextColor(autoText, color: .rgba(r: 0.122, g: 0.161, b: 0.216, a: 1.0)) try engine.block.setPositionX(autoText, value: 40) try engine.block.setPositionY(autoText, value: 30) let wrappedText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: wrappedText) try engine.block.setWidthMode(wrappedText, mode: .absolute) try engine.block.setWidth(wrappedText, value: 320) try engine.block.setHeightMode(wrappedText, mode: .auto) try engine.block.replaceText( wrappedText, text: "Fixed width and auto height, so this text wraps to multiple lines.", ) try engine.block.setTextFontSize(wrappedText, fontSize: 28) try engine.block.setTextColor(wrappedText, color: .rgba(r: 0.200, g: 0.255, b: 0.333, a: 1.0)) try engine.block.setPositionX(wrappedText, value: 40) try engine.block.setPositionY(wrappedText, value: 110) let widthMode = try engine.block.getWidthMode(autoText) let heightMode = try engine.block.getHeightMode(autoText) print("Auto text size modes — width is .auto:", widthMode == .auto) print("Auto text size modes — height is .auto:", heightMode == .auto) let scaledText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: scaledText) try engine.block.setWidthMode(scaledText, mode: .absolute) try engine.block.setHeightMode(scaledText, mode: .absolute) try engine.block.setWidth(scaledText, value: 320) try engine.block.setHeight(scaledText, value: 70) try engine.block.setBool(scaledText, property: "text/automaticFontSizeEnabled", value: true) try engine.block.replaceText(scaledText, text: "Auto-scaled font") try engine.block.setTextColor(scaledText, color: .rgba(r: 0.114, g: 0.306, b: 0.847, a: 1.0)) try engine.block.setPositionX(scaledText, value: 40) try engine.block.setPositionY(scaledText, value: 340) let constrainedText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: constrainedText) try engine.block.setWidthMode(constrainedText, mode: .absolute) try engine.block.setHeightMode(constrainedText, mode: .absolute) try engine.block.setWidth(constrainedText, value: 320) try engine.block.setHeight(constrainedText, value: 70) try engine.block.setBool(constrainedText, property: "text/automaticFontSizeEnabled", value: true) try engine.block.setFloat(constrainedText, property: "text/minAutomaticFontSize", value: 12) try engine.block.setFloat(constrainedText, property: "text/maxAutomaticFontSize", value: 48) try engine.block.replaceText( constrainedText, text: "Edit this text to see automatic font scaling in a 12-48 pt range", ) try engine.block.setTextColor(constrainedText, color: .rgba(r: 0.486, g: 0.176, b: 0.071, a: 1.0)) try engine.block.setPositionX(constrainedText, value: 40) try engine.block.setPositionY(constrainedText, value: 440) let isAutomaticFontSizeEnabled = try engine.block.getBool( scaledText, property: "text/automaticFontSizeEnabled", ) let minAutomaticFontSize = try engine.block.getFloat( constrainedText, property: "text/minAutomaticFontSize", ) let maxAutomaticFontSize = try engine.block.getFloat( constrainedText, property: "text/maxAutomaticFontSize", ) print("Automatic font size enabled:", isAutomaticFontSizeEnabled) print("Automatic font size range:", minAutomaticFontSize, "-", maxAutomaticFontSize) let clippedText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: clippedText) try engine.block.setWidthMode(clippedText, mode: .absolute) try engine.block.setHeightMode(clippedText, mode: .absolute) try engine.block.setWidth(clippedText, value: 320) try engine.block.setHeight(clippedText, value: 60) try engine.block.replaceText( clippedText, text: "This line fits.\nThis line overflows.\nThis line is clipped.", ) try engine.block.setTextFontSize(clippedText, fontSize: 32) try engine.block.setBool(clippedText, property: "text/clipLinesOutsideOfFrame", value: true) try engine.block.setTextColor(clippedText, color: .rgba(r: 0.6, g: 0.106, b: 0.106, a: 1.0)) try engine.block.setPositionX(clippedText, value: 440) try engine.block.setPositionY(clippedText, value: 110) try await engine.captureGuide(page, label: "hero") try await Task.sleep(nanoseconds: 16_000_000) let hasClippedLines = try engine.block.getBool( clippedText, property: "text/hasClippedLines", ) print("Clipped lines detected:", hasClippedLines) } ``` Configure text blocks to automatically adapt their dimensions or font size for dynamic content. ![Text auto-size example showing text blocks with auto dimensions, automatic font sizing, and clipped overflow](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-text-auto-size) CE.SDK provides two approaches for handling dynamic text content. Auto size modes let text blocks resize to fit their content, while automatic font sizing scales the font to fit within fixed boundaries. This guide covers configuring size modes, enabling automatic font sizing, setting font size constraints, and detecting clipped text with the Block API. ## Text Size Modes Text blocks support `SizeMode.absolute`, `SizeMode.percent`, and `SizeMode.auto` for each dimension. Use `engine.block.setWidthMode(_:mode:)` and `engine.block.setHeightMode(_:mode:)` to decide whether the block keeps an explicit size, follows its parent, or grows from its content. ### Auto Width and Height When both dimensions use `SizeMode.auto`, the text block expands freely to fit its content. This works well for single-line labels that grow horizontally. ```swift highlight-auto-width-height let autoText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: autoText) try engine.block.setWidthMode(autoText, mode: .auto) try engine.block.setHeightMode(autoText, mode: .auto) try engine.block.replaceText(autoText, text: "Auto-sized text") try engine.block.setTextFontSize(autoText, fontSize: 36) try engine.block.setTextColor(autoText, color: .rgba(r: 0.122, g: 0.161, b: 0.216, a: 1.0)) try engine.block.setPositionX(autoText, value: 40) try engine.block.setPositionY(autoText, value: 30) ``` Set both width and height modes to `SizeMode.auto`, then write the text and choose a font size. CE.SDK calculates the block dimensions from the rendered text. ### Fixed Width with Auto Height For wrapped multi-line text, keep the width fixed and let the height adjust. The text wraps within the defined width and the block grows vertically as needed. ```swift highlight-fixed-width-auto-height let wrappedText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: wrappedText) try engine.block.setWidthMode(wrappedText, mode: .absolute) try engine.block.setWidth(wrappedText, value: 320) try engine.block.setHeightMode(wrappedText, mode: .auto) try engine.block.replaceText( wrappedText, text: "Fixed width and auto height, so this text wraps to multiple lines.", ) try engine.block.setTextFontSize(wrappedText, fontSize: 28) try engine.block.setTextColor(wrappedText, color: .rgba(r: 0.200, g: 0.255, b: 0.333, a: 1.0)) try engine.block.setPositionX(wrappedText, value: 40) try engine.block.setPositionY(wrappedText, value: 110) ``` This pattern is useful for template placeholders where the width should stay aligned with the design but content length varies. ### Querying Size Modes Read current modes with `engine.block.getWidthMode(_:)` and `engine.block.getHeightMode(_:)`. ```swift highlight-query-size-modes let widthMode = try engine.block.getWidthMode(autoText) let heightMode = try engine.block.getHeightMode(autoText) print("Auto text size modes — width is .auto:", widthMode == .auto) print("Auto text size modes — height is .auto:", heightMode == .auto) ``` ## Automatic Font Sizing For text blocks with bounded dimensions, enable automatic font sizing to scale the font within the frame. The engine calculates the largest font size that fits the content. ```swift highlight-automatic-font-sizing let scaledText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: scaledText) try engine.block.setWidthMode(scaledText, mode: .absolute) try engine.block.setHeightMode(scaledText, mode: .absolute) try engine.block.setWidth(scaledText, value: 320) try engine.block.setHeight(scaledText, value: 70) try engine.block.setBool(scaledText, property: "text/automaticFontSizeEnabled", value: true) try engine.block.replaceText(scaledText, text: "Auto-scaled font") try engine.block.setTextColor(scaledText, color: .rgba(r: 0.114, g: 0.306, b: 0.847, a: 1.0)) try engine.block.setPositionX(scaledText, value: 40) try engine.block.setPositionY(scaledText, value: 340) ``` The example uses `SizeMode.absolute` for both dimensions before enabling `text/automaticFontSizeEnabled`. `SizeMode.percent` can also work when the parent provides concrete dimensions; automatic font sizing only has a bounded frame to solve against when neither dimension is `SizeMode.auto`. ### Setting Font Size Constraints Constrain the automatic scaling range with `text/minAutomaticFontSize` and `text/maxAutomaticFontSize`. These constraints apply to the automatic sizing algorithm, not to manual font size edits made elsewhere in your app. ```swift highlight-font-size-constraints let constrainedText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: constrainedText) try engine.block.setWidthMode(constrainedText, mode: .absolute) try engine.block.setHeightMode(constrainedText, mode: .absolute) try engine.block.setWidth(constrainedText, value: 320) try engine.block.setHeight(constrainedText, value: 70) try engine.block.setBool(constrainedText, property: "text/automaticFontSizeEnabled", value: true) try engine.block.setFloat(constrainedText, property: "text/minAutomaticFontSize", value: 12) try engine.block.setFloat(constrainedText, property: "text/maxAutomaticFontSize", value: 48) try engine.block.replaceText( constrainedText, text: "Edit this text to see automatic font scaling in a 12-48 pt range", ) try engine.block.setTextColor(constrainedText, color: .rgba(r: 0.486, g: 0.176, b: 0.071, a: 1.0)) try engine.block.setPositionX(constrainedText, value: 40) try engine.block.setPositionY(constrainedText, value: 440) ``` With constraints set, the font size scales within the 12-48 pt range as content length changes. Use minimum values to keep dynamic text readable and maximum values to preserve the design hierarchy. Read whether automatic font sizing is enabled with `engine.block.getBool(_:property:)` and the constraint values with `engine.block.getFloat(_:property:)`. ```swift highlight-query-automatic-font-size let isAutomaticFontSizeEnabled = try engine.block.getBool( scaledText, property: "text/automaticFontSizeEnabled", ) let minAutomaticFontSize = try engine.block.getFloat( constrainedText, property: "text/minAutomaticFontSize", ) let maxAutomaticFontSize = try engine.block.getFloat( constrainedText, property: "text/maxAutomaticFontSize", ) print("Automatic font size enabled:", isAutomaticFontSizeEnabled) print("Automatic font size range:", minAutomaticFontSize, "-", maxAutomaticFontSize) ``` ## Text Clipping New text blocks clip overflowing lines by default. Use a bounded frame when text must stay inside a fixed area, keep or set `text/clipLinesOutsideOfFrame` to `true`, then read `text/hasClippedLines` after the engine has updated text layout. ```swift highlight-text-clipping let clippedText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: clippedText) try engine.block.setWidthMode(clippedText, mode: .absolute) try engine.block.setHeightMode(clippedText, mode: .absolute) try engine.block.setWidth(clippedText, value: 320) try engine.block.setHeight(clippedText, value: 60) try engine.block.replaceText( clippedText, text: "This line fits.\nThis line overflows.\nThis line is clipped.", ) try engine.block.setTextFontSize(clippedText, fontSize: 32) try engine.block.setBool(clippedText, property: "text/clipLinesOutsideOfFrame", value: true) try engine.block.setTextColor(clippedText, color: .rgba(r: 0.6, g: 0.106, b: 0.106, a: 1.0)) try engine.block.setPositionX(clippedText, value: 440) try engine.block.setPositionY(clippedText, value: 110) ``` `text/hasClippedLines` is layout-backed — the engine recomputes it during the layout pass that follows a text or frame change, so yield briefly before reading it. ```swift highlight-text-clipping-query try await Task.sleep(nanoseconds: 16_000_000) let hasClippedLines = try engine.block.getBool( clippedText, property: "text/hasClippedLines", ) print("Clipped lines detected:", hasClippedLines) ``` The clipping readback helps you decide whether to enable automatic font sizing, switch the height mode to `SizeMode.auto`, or adjust the frame dimensions. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.create(_:)` | Create a text block with `.text`. | | `engine.block.appendChild(to:child:)` | Add the text block to the scene hierarchy. | | `engine.block.replaceText(_:text:in:)` | Set the full text content. | | `engine.block.setWidthMode(_:mode:)` | Set the text block's width mode. | | `engine.block.getWidthMode(_:)` | Read the current width mode. | | `engine.block.setHeightMode(_:mode:)` | Set the text block's height mode. | | `engine.block.getHeightMode(_:)` | Read the current height mode. | | `engine.block.setWidth(_:value:maintainCrop:)` | Set an explicit text block width. | | `engine.block.setHeight(_:value:maintainCrop:)` | Set an explicit text block height. | | `engine.block.setPositionX(_:value:)` | Position the text block on the x-axis. | | `engine.block.setPositionY(_:value:)` | Position the text block on the y-axis. | | `engine.block.setTextFontSize(_:fontSize:in:)` | Set the base text font size. | | `engine.block.setTextColor(_:color:in:)` | Set the text color. | | `engine.block.setBool(_:property:value:)` | Set a boolean property such as `text/automaticFontSizeEnabled` or `text/clipLinesOutsideOfFrame`. | | `engine.block.getBool(_:property:)` | Read a boolean property such as `text/automaticFontSizeEnabled` or `text/hasClippedLines`. | | `engine.block.setFloat(_:property:value:)` | Set a numeric property such as `text/minAutomaticFontSize` or `text/maxAutomaticFontSize`. | | `engine.block.getFloat(_:property:)` | Read a numeric property such as `text/minAutomaticFontSize` or `text/maxAutomaticFontSize`. | ### Properties | Property | Type | Description | | --- | --- | --- | | `text/automaticFontSizeEnabled` | Bool | Enable or disable automatic font sizing. Defaults to `false`. | | `text/minAutomaticFontSize` | Float | Minimum font size for automatic sizing. Negative values indicate no minimum. | | `text/maxAutomaticFontSize` | Float | Maximum font size for automatic sizing. Negative values indicate no maximum. | | `text/clipLinesOutsideOfFrame` | Bool | Whether lines that fall outside the frame are hidden. Defaults to `true`. | | `text/hasClippedLines` | Bool | Reports whether any lines are currently clipped. Updates during layout. | ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Text does not resize | One dimension still uses `SizeMode.absolute` | Set both dimensions to `SizeMode.auto` for free-growing text, or combine fixed width with `SizeMode.auto` height for wrapped text | | Font size does not scale | The text block does not have bounded width and height, or `text/automaticFontSizeEnabled` is disabled | Use non-auto width and height modes such as `SizeMode.absolute`, or `SizeMode.percent` with a concrete parent size, then enable automatic font sizing | | Text becomes too small | The automatic font size range has no readable lower bound | Set `text/minAutomaticFontSize` to the smallest acceptable size for the design | | Text is clipped unexpectedly | A fixed frame is hiding overflow without automatic font sizing | Check `text/hasClippedLines`, enable automatic font sizing, or switch the height mode to `SizeMode.auto` | ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) — Apply fonts, colors, alignment, and other styling options to customize text appearance. - [Adjust Text Spacing](https://img.ly/docs/cesdk/mac-catalyst/text/adjust-spacing-c1a3b6/) — Control letter spacing, line height, and paragraph spacing in text blocks. - [Customize Fonts](#broken-link-9565b3) — On iOS, load and manage custom fonts to match brand guidelines or user preferences. --- ## 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 Decorations" description: "Add underline, strikethrough, and overline decorations to text with customizable styles, colors, and thickness." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/decorations-d3c0a1/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Text Decorations](https://img.ly/docs/cesdk/mac-catalyst/text/decorations-d3c0a1/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-decorations/TextDecorations.swift reference-only import Foundation import IMGLYEngine @MainActor func textDecorations(engine: Engine) async throws { let scene = try engine.scene.create() let text = try engine.block.create(.text) try engine.block.appendChild(to: scene, child: text) try engine.block.setWidthMode(text, mode: .auto) try engine.block.setHeightMode(text, mode: .auto) try engine.block.replaceText(text, text: "Hello CE.SDK") // Toggle underline on the entire text try engine.block.toggleTextDecorationUnderline(text) // Toggle strikethrough on the entire text try engine.block.toggleTextDecorationStrikethrough(text) // Toggle overline on the entire text try engine.block.toggleTextDecorationOverline(text) // Calling toggle again removes the decoration try engine.block.toggleTextDecorationOverline(text) // Query the current decoration configurations // Returns a list of unique TextDecorationConfig values in the range let decorations = try engine.block.getTextDecorations(text) // Each config contains: line, style, underlineColor, underlineThickness, underlineOffset, skipInk // Set a specific decoration style // Available styles: .solid, .double, .dotted, .dashed, .wavy try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, style: .dashed, )) // Set a custom underline color (only applies to underlines) // Strikethrough and overline always use the text color try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, underlineColor: .rgba(r: 1, g: 0, b: 0, a: 1), )) // Adjust the underline thickness // Default is 1.0, values above 1.0 make the line thicker try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, underlineThickness: 2.0, )) // Adjust the underline position relative to the font default // 0 = font default, positive values move further from baseline, negative values move closer try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, underlineOffset: 0.1, )) // Apply decorations to a specific character range using Range let currentText = try engine.block.getString(text, property: "text/text") let helloRange = currentText.startIndex ..< currentText.index(currentText.startIndex, offsetBy: 5) // Toggle underline on "Hello" try engine.block.toggleTextDecorationUnderline(text, in: helloRange) // Query decorations in a specific range let subrangeDecorations = try engine.block.getTextDecorations(text, in: helloRange) // Combine multiple decoration lines on the same text // All active lines share the same style and thickness try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: [.underline, .strikethrough], )) // Remove all decorations try engine.block.setTextDecoration(text, config: TextDecorationConfig()) _ = decorations _ = subrangeDecorations } ``` Add underline, strikethrough, and overline decorations to text blocks with configurable styles, colors, and thickness. CE.SDK supports three types of text decorations: underline, strikethrough, and overline. Decorations can be toggled on and off, customized with different line styles, and applied to specific character ranges. `TextDecorationLine` is an `OptionSet`, so multiple lines can be combined using set operations. ## Toggle Decorations Toggle decorations using `engine.block.toggleTextDecorationUnderline()`, `engine.block.toggleTextDecorationStrikethrough()`, and `engine.block.toggleTextDecorationOverline()`. If all characters in the range already have the decoration, it is removed; otherwise, it is added to all. ```swift highlight-toggle-decorations // Toggle underline on the entire text try engine.block.toggleTextDecorationUnderline(text) // Toggle strikethrough on the entire text try engine.block.toggleTextDecorationStrikethrough(text) // Toggle overline on the entire text try engine.block.toggleTextDecorationOverline(text) // Calling toggle again removes the decoration try engine.block.toggleTextDecorationOverline(text) ``` ## Query Decorations Query the current decorations using `engine.block.getTextDecorations()`. It returns an array of unique `TextDecorationConfig` values. Each config includes the active `line` (an `OptionSet`), `style`, optional `underlineColor`, `underlineThickness`, `underlineOffset`, and `skipInk`. ```swift highlight-query-decorations // Query the current decoration configurations // Returns a list of unique TextDecorationConfig values in the range let decorations = try engine.block.getTextDecorations(text) // Each config contains: line, style, underlineColor, underlineThickness, underlineOffset, skipInk ``` ## Custom Decoration Styles Set a specific decoration style using `engine.block.setTextDecoration()` with a `TextDecorationConfig`. Available styles are `.solid` (default), `.double`, `.dotted`, `.dashed`, and `.wavy`. ```swift highlight-custom-style // Set a specific decoration style // Available styles: .solid, .double, .dotted, .dashed, .wavy try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, style: .dashed, )) ``` ## Underline Color Set a custom underline color that differs from the text color. The `underlineColor` property only applies to underlines; strikethrough and overline always use the text color. ```swift highlight-underline-color // Set a custom underline color (only applies to underlines) // Strikethrough and overline always use the text color try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, underlineColor: .rgba(r: 1, g: 0, b: 0, a: 1), )) ``` ## Decoration Thickness Adjust the underline thickness using the `underlineThickness` property. The default is `1.0`. Values above `1.0` make the underline thicker. ```swift highlight-thickness // Adjust the underline thickness // Default is 1.0, values above 1.0 make the line thicker try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, underlineThickness: 2.0, )) ``` ## Underline Offset Adjust the underline position using the `underlineOffset` property, which acts as a relative multiplier on the font-default distance. The actual position is computed as `fontDefault * (1 + underlineOffset)`. The default is `0`, which uses the font's default underline position. Positive values move the underline proportionally further from the baseline, negative values move it proportionally closer. ```swift highlight-offset // Adjust the underline position relative to the font default // 0 = font default, positive values move further from baseline, negative values move closer try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, underlineOffset: 0.1, )) ``` ## Subrange Decorations Apply decorations to a specific character range using `Range`. Both toggle and set operations accept an optional `in` parameter for subrange targeting. ```swift highlight-subrange // Apply decorations to a specific character range using Range let currentText = try engine.block.getString(text, property: "text/text") let helloRange = currentText.startIndex ..< currentText.index(currentText.startIndex, offsetBy: 5) // Toggle underline on "Hello" try engine.block.toggleTextDecorationUnderline(text, in: helloRange) // Query decorations in a specific range let subrangeDecorations = try engine.block.getTextDecorations(text, in: helloRange) ``` ## Combine Decorations Combine multiple decoration types using the `TextDecorationLine` `OptionSet`. Pass a set like `[.underline, .strikethrough]` to apply both simultaneously. All active lines share the same style and thickness. ```swift highlight-combine // Combine multiple decoration lines on the same text // All active lines share the same style and thickness try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: [.underline, .strikethrough], )) ``` ## Remove Decorations Remove all decorations by creating a default `TextDecorationConfig()`, which sets the line to `.none`. ```swift highlight-remove // Remove all decorations try engine.block.setTextDecoration(text, config: TextDecorationConfig()) ``` ## Full Code Here's the full code: ```swift highlight-text-decorations import Foundation import IMGLYEngine @MainActor func textDecorations(engine: Engine) async throws { let scene = try engine.scene.create() let text = try engine.block.create(.text) try engine.block.appendChild(to: scene, child: text) try engine.block.setWidthMode(text, mode: .auto) try engine.block.setHeightMode(text, mode: .auto) try engine.block.replaceText(text, text: "Hello CE.SDK") // Toggle underline on the entire text try engine.block.toggleTextDecorationUnderline(text) // Toggle strikethrough on the entire text try engine.block.toggleTextDecorationStrikethrough(text) // Toggle overline on the entire text try engine.block.toggleTextDecorationOverline(text) // Calling toggle again removes the decoration try engine.block.toggleTextDecorationOverline(text) // Query the current decoration configurations // Returns a list of unique TextDecorationConfig values in the range let decorations = try engine.block.getTextDecorations(text) // Each config contains: line, style, underlineColor, underlineThickness, underlineOffset, skipInk // Set a specific decoration style // Available styles: .solid, .double, .dotted, .dashed, .wavy try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, style: .dashed, )) // Set a custom underline color (only applies to underlines) // Strikethrough and overline always use the text color try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, underlineColor: .rgba(r: 1, g: 0, b: 0, a: 1), )) // Adjust the underline thickness // Default is 1.0, values above 1.0 make the line thicker try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, underlineThickness: 2.0, )) // Adjust the underline position relative to the font default // 0 = font default, positive values move further from baseline, negative values move closer try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: .underline, underlineOffset: 0.1, )) // Apply decorations to a specific character range using Range let currentText = try engine.block.getString(text, property: "text/text") let helloRange = currentText.startIndex ..< currentText.index(currentText.startIndex, offsetBy: 5) // Toggle underline on "Hello" try engine.block.toggleTextDecorationUnderline(text, in: helloRange) // Query decorations in a specific range let subrangeDecorations = try engine.block.getTextDecorations(text, in: helloRange) // Combine multiple decoration lines on the same text // All active lines share the same style and thickness try engine.block.setTextDecoration(text, config: TextDecorationConfig( line: [.underline, .strikethrough], )) // Remove all decorations try engine.block.setTextDecoration(text, config: TextDecorationConfig()) _ = decorations _ = subrangeDecorations } ``` --- ## 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 Text" description: "Edit text content programmatically with range-based APIs for replacing, formatting, and querying text." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/edit-c5106b/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Edit Text](https://img.ly/docs/cesdk/mac-catalyst/text/edit-c5106b/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-edit/TextEdit.swift reference-only import Foundation import IMGLYEngine @MainActor func textEdit(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL // Demo scaffolding: a Pixel-unit page sized for a single styled headline so // the formatting changes are visible in the captured hero export. let scene = try engine.scene.create(designUnit: .px) 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: 400) // Create a text block and position it on the page. let text = try engine.block.create(.text) try engine.block.appendChild(to: page, child: text) try engine.block.setPositionX(text, value: 100) try engine.block.setPositionY(text, value: 150) try engine.block.setWidthMode(text, mode: .auto) try engine.block.setHeightMode(text, mode: .auto) // Define a Roboto typeface with regular, bold, italic, and bold-italic variants. // Each variant the example formats with later (bold / italic / etc.) needs a matching font in the typeface. let robotoBase = baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto") let typeface = 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, ), ], ) try engine.block.setFont(text, fontFileURL: typeface.fonts[0].uri, typeface: typeface) try engine.block.setTextFontSize(text, fontSize: 80) // Replace the entire text content. try engine.block.replaceText(text, text: "Hello World!") // Replace "World" with "CE.SDK". try engine.block.replaceText(text, text: "CE.SDK", in: "Hello World!".range(of: "World")!) // Insert " Guide" before the exclamation mark. let insertion = "Hello CE.SDK!".range(of: "!")!.lowerBound try engine.block.replaceText(text, text: " Guide", in: insertion ..< insertion) // Remove "Hello " to leave "CE.SDK Guide!". try engine.block.removeText(text, from: "Hello CE.SDK Guide!".range(of: "Hello ")!) // Apply bold weight to "CE.SDK". try engine.block.setTextFontWeight( text, fontWeight: .bold, in: "CE.SDK Guide!".range(of: "CE.SDK")!, ) // Apply a blue color to "Guide". try engine.block.setTextColor( text, color: .rgba(r: 0.2, g: 0.6, b: 1.0, a: 1.0), in: "CE.SDK Guide!".range(of: "Guide")!, ) // Apply italic style to "Guide". try engine.block.setTextFontStyle( text, fontStyle: .italic, in: "CE.SDK Guide!".range(of: "Guide")!, ) // Uppercase the "Guide" range. try engine.block.setTextCase( text, textCase: .uppercase, in: "CE.SDK Guide!".range(of: "Guide")!, ) try await engine.captureGuide(page, label: "hero") if try engine.block.canToggleBoldFont(text, in: "CE.SDK Guide!".range(of: "Guide")!) { try engine.block.toggleBoldFont(text, in: "CE.SDK Guide!".range(of: "Guide")!) } if try engine.block.canToggleItalicFont(text, in: "CE.SDK Guide!".range(of: "CE.SDK")!) { try engine.block.toggleItalicFont(text, in: "CE.SDK Guide!".range(of: "CE.SDK")!) } let colors = try engine.block.getTextColors(text) let weights = try engine.block.getTextFontWeights(text) let styles = try engine.block.getTextFontStyles(text) let sizes = try engine.block.getTextFontSizes(text) let cases = try engine.block.getTextCases(text) print("Colors: \(colors)") print("Weights: \(weights), styles: \(styles)") print("Sizes: \(sizes), cases: \(cases)") // Apply a different typeface to a range, preserving formatting where possible. try engine.block.setTypeface(text, typeface: typeface, in: "CE.SDK Guide!".range(of: "Guide")!) // Read back the block's base typeface and the unique typefaces in the block. let baseTypeface = try engine.block.getTypeface(text) let typefacesInBlock = try engine.block.getTypefaces(text) print("Base typeface: \(baseTypeface.name), unique typefaces in block: \(typefacesInBlock.count)") let lineCount = try engine.block.getTextVisibleLineCount(text) for index in 0 ..< lineCount { let content = try engine.block.getTextVisibleLineContent(text, lineIndex: index) let bounds = try engine.block.getTextLineBoundingBoxRect(text, index: index) print("Line \(index): \"\(content)\" at \(bounds)") } let metrics = try await engine.editor.getFontMetrics(fontFileURI: typeface.fonts[0].uri.absoluteString) print( "Ascender: \(metrics.ascender), descender: \(metrics.descender), unitsPerEm: \(metrics.unitsPerEm)", ) print( "Cap height: \(metrics.capHeight), x-height: \(metrics.xHeight), line gap: \(metrics.lineGap)", ) } ``` Edit text content programmatically with range-based APIs for replacing, formatting, and querying text. ![A text block rendered with mixed formatting: "CE.SDK" in bold and "GUIDE" in blue italic uppercase.](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-text-edit) CE.SDK provides text editing through range-based APIs that operate on Swift `Range` values. This guide covers replacing and removing text content, applying formatting to character ranges, querying text properties, and retrieving line and font information. ## Creating a Text Block Create a text block and add it to the page. Set both width and height modes to `.auto` so the block resizes around its content. ```swift highlight-textEdit-createText // Create a text block and position it on the page. let text = try engine.block.create(.text) try engine.block.appendChild(to: page, child: text) try engine.block.setPositionX(text, value: 100) try engine.block.setPositionY(text, value: 150) try engine.block.setWidthMode(text, mode: .auto) try engine.block.setHeightMode(text, mode: .auto) ``` ## Setting a Typeface Define a typeface with the font variants the example needs and apply it with `setFont(_:fontFileURL:typeface:)`. Including a bold variant in the typeface is what later lets `setTextFontWeight(_:fontWeight:in:)` and `toggleBoldFont(_:in:)` switch a range to bold. ```swift highlight-textEdit-setTypeface // Define a Roboto typeface with regular, bold, italic, and bold-italic variants. // Each variant the example formats with later (bold / italic / etc.) needs a matching font in the typeface. let robotoBase = baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto") let typeface = 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, ), ], ) try engine.block.setFont(text, fontFileURL: typeface.fonts[0].uri, typeface: typeface) try engine.block.setTextFontSize(text, fontSize: 80) ``` ## Replacing and Removing Text Use `replaceText(_:text:in:)` to swap or insert text. Passing a zero-length subrange inserts at that position; passing a non-zero range replaces it. Omitting the subrange replaces the entire content. ```swift highlight-textEdit-replaceText // Replace the entire text content. try engine.block.replaceText(text, text: "Hello World!") // Replace "World" with "CE.SDK". try engine.block.replaceText(text, text: "CE.SDK", in: "Hello World!".range(of: "World")!) // Insert " Guide" before the exclamation mark. let insertion = "Hello CE.SDK!".range(of: "!")!.lowerBound try engine.block.replaceText(text, text: " Guide", in: insertion ..< insertion) ``` Use `removeText(_:from:)` to delete a character range. Omitting the subrange clears the block. ```swift highlight-textEdit-removeText // Remove "Hello " to leave "CE.SDK Guide!". try engine.block.removeText(text, from: "Hello CE.SDK Guide!".range(of: "Hello ")!) ``` The example builds the `Range` arguments from the expected text content (`"Hello World!"` → `"Hello CE.SDK!"` → `"Hello CE.SDK Guide!"`). In an interactive editor, pass the user's current selection instead. ## Applying Text Formatting Apply formatting to specific character ranges with the `setText*` setters. Each accepts an optional `in:` subrange — omit it to format the entire block. ```swift highlight-textEdit-setFormatting // Apply bold weight to "CE.SDK". try engine.block.setTextFontWeight( text, fontWeight: .bold, in: "CE.SDK Guide!".range(of: "CE.SDK")!, ) // Apply a blue color to "Guide". try engine.block.setTextColor( text, color: .rgba(r: 0.2, g: 0.6, b: 1.0, a: 1.0), in: "CE.SDK Guide!".range(of: "Guide")!, ) // Apply italic style to "Guide". try engine.block.setTextFontStyle( text, fontStyle: .italic, in: "CE.SDK Guide!".range(of: "Guide")!, ) // Uppercase the "Guide" range. try engine.block.setTextCase( text, textCase: .uppercase, in: "CE.SDK Guide!".range(of: "Guide")!, ) ``` - `setTextFontWeight(_:fontWeight:in:)` applies a `FontWeight` such as `.bold` or `.normal`. - `setTextColor(_:color:in:)` takes a `Color`; `.rgba(r:g:b:a:)` constructs an sRGB value. - `setTextFontStyle(_:fontStyle:in:)` switches between `.normal` and `.italic`. - `setTextFontSize(_:fontSize:in:)` updates the font size in the scene's font-size unit; when applied to the whole block, the block's font size is updated. - `setTextCase(_:textCase:in:)` applies `.titlecase`, `.uppercase`, `.lowercase`, or `.normal`. `toggleBoldFont(_:in:)` and `toggleItalicFont(_:in:)` flip the weight or style of a range. Gate each call on the matching `canToggle*Font(_:in:)` check, which returns `false` when the active typeface lacks a corresponding variant. ```swift highlight-textEdit-toggleFormatting if try engine.block.canToggleBoldFont(text, in: "CE.SDK Guide!".range(of: "Guide")!) { try engine.block.toggleBoldFont(text, in: "CE.SDK Guide!".range(of: "Guide")!) } if try engine.block.canToggleItalicFont(text, in: "CE.SDK Guide!".range(of: "CE.SDK")!) { try engine.block.toggleItalicFont(text, in: "CE.SDK Guide!".range(of: "CE.SDK")!) } ``` Use `setTypeface(_:typeface:in:)` to apply a typeface to the block or to a specific range. When the new typeface differs from the current one, existing weights and styles are preserved where possible, falling back to the closest available variant. `setFont(_:fontFileURL:typeface:)`, in contrast, resets the block's formatting. ```swift highlight-textEdit-typefaceManagement // Apply a different typeface to a range, preserving formatting where possible. try engine.block.setTypeface(text, typeface: typeface, in: "CE.SDK Guide!".range(of: "Guide")!) // Read back the block's base typeface and the unique typefaces in the block. let baseTypeface = try engine.block.getTypeface(text) let typefacesInBlock = try engine.block.getTypefaces(text) print("Base typeface: \(baseTypeface.name), unique typefaces in block: \(typefacesInBlock.count)") ``` ## Querying Text Properties Each `getText*` getter returns an array of the unique values found in the queried range — text blocks can contain mixed formatting, so the array length tells you whether the range is uniform. ```swift highlight-textEdit-queryFormatting let colors = try engine.block.getTextColors(text) let weights = try engine.block.getTextFontWeights(text) let styles = try engine.block.getTextFontStyles(text) let sizes = try engine.block.getTextFontSizes(text) let cases = try engine.block.getTextCases(text) print("Colors: \(colors)") print("Weights: \(weights), styles: \(styles)") print("Sizes: \(sizes), cases: \(cases)") ``` `getTypeface(_:)` returns the block's base typeface, and `getTypefaces(_:in:)` returns the unique typefaces used by the runs in a range. Omit the subrange on any getter to query the entire block. ## Line Information After the block has rendered, query its visible lines. `getTextVisibleLineCount(_:)` returns the number of rendered lines, `getTextVisibleLineContent(_:lineIndex:)` returns the line's text, and `getTextLineBoundingBoxRect(_:index:)` returns the line's bounding box in the scene's global coordinate space. ```swift highlight-textEdit-lineInfo let lineCount = try engine.block.getTextVisibleLineCount(text) for index in 0 ..< lineCount { let content = try engine.block.getTextVisibleLineContent(text, lineIndex: index) let bounds = try engine.block.getTextLineBoundingBoxRect(text, index: index) print("Line \(index): \"\(content)\" at \(bounds)") } ``` ## Font Metrics `engine.editor.getFontMetrics(fontFileURI:)` returns the font's raw design-unit metrics. The font is fetched asynchronously if it has not been loaded yet. ```swift highlight-textEdit-fontMetrics let metrics = try await engine.editor.getFontMetrics(fontFileURI: typeface.fonts[0].uri.absoluteString) print( "Ascender: \(metrics.ascender), descender: \(metrics.descender), unitsPerEm: \(metrics.unitsPerEm)", ) print( "Cap height: \(metrics.capHeight), x-height: \(metrics.xHeight), line gap: \(metrics.lineGap)", ) ``` The returned `FontMetrics` value exposes `ascender`, `descender`, `unitsPerEm`, `lineGap`, `capHeight`, `xHeight`, `underlineOffset`, `underlineSize`, `strikeoutOffset`, and `strikeoutSize`. Use these to compute line heights, align mixed-typeface runs, or position decorations. ## Troubleshooting **Range indices unexpected**: Build ranges from the current text content. After a `replaceText` or `removeText` call, the indices into the old string are no longer valid. **Formatting not applied**: Confirm the active typeface ships a variant for the requested weight or style. `canToggleBoldFont(_:in:)` and `canToggleItalicFont(_:in:)` return `false` when no matching variant is available. **Typeface change loses formatting**: `setFont(_:fontFileURL:typeface:)` resets the block's formatting. Use `setTypeface(_:typeface:in:)` to preserve weights and styles where possible. **Line count is zero**: The block has to render before line information is available. Empty text blocks report zero lines. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.setFont(_:fontFileURL:typeface:)` | Set font and typeface, resetting formatting | | `engine.block.setTypeface(_:typeface:in:)` | Change typeface, preserving formatting where possible | | `engine.block.getTypeface(_:)` | Get the block's base typeface | | `engine.block.getTypefaces(_:in:)` | Get unique typefaces in a range | | `engine.block.replaceText(_:text:in:)` | Replace or insert text at a range | | `engine.block.removeText(_:from:)` | Remove text in a range | | `engine.block.setTextColor(_:color:in:)` | Set color for a range | | `engine.block.getTextColors(_:in:)` | Get unique colors in a range | | `engine.block.setTextFontWeight(_:fontWeight:in:)` | Set font weight for a range | | `engine.block.getTextFontWeights(_:in:)` | Get unique font weights in a range | | `engine.block.setTextFontStyle(_:fontStyle:in:)` | Set font style for a range | | `engine.block.getTextFontStyles(_:in:)` | Get unique font styles in a range | | `engine.block.setTextFontSize(_:fontSize:in:)` | Set font size for a range | | `engine.block.getTextFontSizes(_:in:)` | Get unique font sizes in a range | | `engine.block.setTextCase(_:textCase:in:)` | Set text case for a range | | `engine.block.getTextCases(_:in:)` | Get unique text cases in a range | | `engine.block.canToggleBoldFont(_:in:)` | Whether bold can be toggled for a range | | `engine.block.canToggleItalicFont(_:in:)` | Whether italic can be toggled for a range | | `engine.block.toggleBoldFont(_:in:)` | Toggle bold for a range | | `engine.block.toggleItalicFont(_:in:)` | Toggle italic for a range | | `engine.block.getTextCursorRange()` | Get the user's grapheme selection range; returns `nil` when no text block is being edited | | `engine.block.setTextCursorRange(_:)` | Set the cursor or selection; throws when no text block is being edited | | `engine.block.getTextVisibleLineCount(_:)` | Number of rendered lines | | `engine.block.getTextVisibleLineContent(_:lineIndex:)` | Text content of a rendered line | | `engine.block.getTextLineBoundingBoxRect(_:index:)` | Bounding box of a rendered line | | `engine.editor.getFontMetrics(fontFileURI:)` | Raw font metrics for a font file URI | ## Next Steps - [Add Text](https://img.ly/docs/cesdk/mac-catalyst/text/add-4f5011/) — Create and configure text blocks - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) — Apply fonts, colors, and formatting - [Text Designs](https://img.ly/docs/cesdk/mac-catalyst/text/text-designs-a1b2c3/) — Create and customize text component libraries using predefined text designs that appear in your asset library - [Auto-Size](https://img.ly/docs/cesdk/mac-catalyst/text/auto-size-5331b3/) — Configure automatic text sizing --- ## 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 Effects" description: "Add visual depth and interest to text blocks using drop shadows and stroke outlines." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/effects-2dc9fc/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Text Effects](https://img.ly/docs/cesdk/mac-catalyst/text/effects-2dc9fc/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-effects/TextEffects.swift reference-only import IMGLYEngine @MainActor func textEffects(engine: Engine) async throws { // Demo scaffolding: an 800×500 page with a light off-white background so the // dark shadow and the blue outline both stay readable. Creating the scene // with `designUnit: .px` pairs the font-size unit to Pixel, so the literal // `setTextFontSize` values below render at the dimensions the layout // positions assume. 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: 500) try engine.block.appendChild(to: scene, child: page) let background = try engine.block.create(.graphic) try engine.block.setShape(background, shape: engine.block.createShape(.rect)) try engine.block.setFill(background, fill: engine.block.createFill(.color)) let backgroundFill = try engine.block.getFill(background) try engine.block.setColor( backgroundFill, property: "fill/color/value", color: .rgba(r: 0.969, g: 0.973, b: 0.984, a: 1.0), ) try engine.block.setWidth(background, value: 800) try engine.block.setHeight(background, value: 500) try engine.block.appendChild(to: page, child: background) let shadowText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: shadowText) try engine.block.replaceText(shadowText, text: "Drop Shadow") try engine.block.setTextFontSize(shadowText, fontSize: 90) try engine.block.setWidthMode(shadowText, mode: .auto) try engine.block.setHeightMode(shadowText, mode: .auto) try engine.block.setPositionX(shadowText, value: 50) try engine.block.setPositionY(shadowText, value: 50) guard try engine.block.supportsDropShadow(shadowText) else { return } try engine.block.setDropShadowEnabled(shadowText, enabled: true) try engine.block.setDropShadowColor(shadowText, color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 0.6)) try engine.block.setDropShadowOffsetX(shadowText, offsetX: 5) try engine.block.setDropShadowOffsetY(shadowText, offsetY: 5) try engine.block.setDropShadowBlurRadiusX(shadowText, blurRadiusX: 10) try engine.block.setDropShadowBlurRadiusY(shadowText, blurRadiusY: 10) try await engine.captureGuide(page, label: "after-drop-shadow") let isDropShadowEnabled = try engine.block.isDropShadowEnabled(shadowText) let dropShadowColor: Color = try engine.block.getDropShadowColor(shadowText) let dropShadowOffsetX = try engine.block.getDropShadowOffsetX(shadowText) let dropShadowOffsetY = try engine.block.getDropShadowOffsetY(shadowText) print("Drop shadow enabled:", isDropShadowEnabled) print("Drop shadow color:", dropShadowColor) print("Drop shadow offset:", dropShadowOffsetX, dropShadowOffsetY) let outlineText = try engine.block.create(.text) try engine.block.appendChild(to: page, child: outlineText) try engine.block.replaceText(outlineText, text: "Outline") try engine.block.setTextFontSize(outlineText, fontSize: 90) try engine.block.setWidthMode(outlineText, mode: .auto) try engine.block.setHeightMode(outlineText, mode: .auto) try engine.block.setPositionX(outlineText, value: 50) try engine.block.setPositionY(outlineText, value: 250) guard try engine.block.supportsStroke(outlineText) else { return } try engine.block.setStrokeEnabled(outlineText, enabled: true) try engine.block.setStrokeWidth(outlineText, width: 2) try engine.block.setStrokeColor(outlineText, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0)) try engine.block.setStrokeStyle(outlineText, style: .solid) try engine.block.setStrokePosition(outlineText, position: .center) let isStrokeEnabled = try engine.block.isStrokeEnabled(outlineText) let strokeWidth = try engine.block.getStrokeWidth(outlineText) let strokeColor: Color = try engine.block.getStrokeColor(outlineText) let strokeStyle = try engine.block.getStrokeStyle(outlineText) let strokePosition = try engine.block.getStrokePosition(outlineText) print("Stroke enabled:", isStrokeEnabled) print("Stroke width:", strokeWidth) print("Stroke color:", strokeColor) print("Stroke style is solid:", strokeStyle == .solid) print("Stroke position is center:", strokePosition == .center) try await engine.captureGuide(page, label: "hero") } ``` Add visual depth and interest to text blocks using drop shadows and stroke outlines. ![Two text blocks on a light background, one with a soft drop shadow and one with a blue stroke outline](./assets/swift-based.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-text-effects) Text effects that apply directly to text blocks include drop shadows for depth and stroke outlines for text borders. These visual effects are distinct from text styling properties like colors, fonts, and backgrounds. This guide covers how to apply text effects programmatically using the Block API. ## Drop Shadows Drop shadows add depth and emphasis to text. Configure shadow color, position, and blur softness with the dedicated drop shadow API on the text block. ```swift highlight-textEffects-dropShadow guard try engine.block.supportsDropShadow(shadowText) else { return } try engine.block.setDropShadowEnabled(shadowText, enabled: true) try engine.block.setDropShadowColor(shadowText, color: .rgba(r: 0.0, g: 0.0, b: 0.0, a: 0.6)) try engine.block.setDropShadowOffsetX(shadowText, offsetX: 5) try engine.block.setDropShadowOffsetY(shadowText, offsetY: 5) try engine.block.setDropShadowBlurRadiusX(shadowText, blurRadiusX: 10) try engine.block.setDropShadowBlurRadiusY(shadowText, blurRadiusY: 10) ``` The offset values position the shadow relative to the text, while the blur radius controls shadow softness. Horizontal and vertical blur can be configured independently for asymmetric effects. ### Reading Shadow Values Query the current drop shadow values from the text block. Annotate `getDropShadowColor` with `: Color` so the compiler picks the non-deprecated overload. ```swift highlight-textEffects-readDropShadow let isDropShadowEnabled = try engine.block.isDropShadowEnabled(shadowText) let dropShadowColor: Color = try engine.block.getDropShadowColor(shadowText) let dropShadowOffsetX = try engine.block.getDropShadowOffsetX(shadowText) let dropShadowOffsetY = try engine.block.getDropShadowOffsetY(shadowText) print("Drop shadow enabled:", isDropShadowEnabled) print("Drop shadow color:", dropShadowColor) print("Drop shadow offset:", dropShadowOffsetX, dropShadowOffsetY) ``` ## Stroke Outlines Stroke outlines add a colored border around text. Enable stroke with `setStrokeEnabled(_:enabled:)`, then configure width, color, style, and position. ```swift highlight-textEffects-stroke guard try engine.block.supportsStroke(outlineText) else { return } try engine.block.setStrokeEnabled(outlineText, enabled: true) try engine.block.setStrokeWidth(outlineText, width: 2) try engine.block.setStrokeColor(outlineText, color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1.0)) try engine.block.setStrokeStyle(outlineText, style: .solid) try engine.block.setStrokePosition(outlineText, position: .center) ``` The stroke width is specified in the scene's design unit. Text blocks support `StrokePosition.center`, `StrokePosition.inner`, and `StrokePosition.outer` via `setStrokePosition(_:position:)`. Stroke styles include `StrokeStyle.solid`, `StrokeStyle.dashed`, `StrokeStyle.dotted`, and other line patterns. ### Reading Stroke Values Query the current stroke values from the text block. As with drop shadow, annotate `getStrokeColor` with `: Color` to select the canonical overload. ```swift highlight-textEffects-readStroke let isStrokeEnabled = try engine.block.isStrokeEnabled(outlineText) let strokeWidth = try engine.block.getStrokeWidth(outlineText) let strokeColor: Color = try engine.block.getStrokeColor(outlineText) let strokeStyle = try engine.block.getStrokeStyle(outlineText) let strokePosition = try engine.block.getStrokePosition(outlineText) print("Stroke enabled:", isStrokeEnabled) print("Stroke width:", strokeWidth) print("Stroke color:", strokeColor) print("Stroke style is solid:", strokeStyle == .solid) print("Stroke position is center:", strokePosition == .center) ``` ## Other Effects Text blocks do not support the blur or effect-stack APIs. Use `supportsBlur(_:)` and `supportsEffects(_:)` before applying those APIs to other block types, and see [Filters & Effects Overview](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/overview-299b15/) for generic block effects. ## API Reference ### Drop Shadow Methods | Method | Description | | --- | --- | | `engine.block.supportsDropShadow(_:)` | Check whether a block supports drop shadows | | `engine.block.setDropShadowEnabled(_:enabled:)` | Enable or disable the drop shadow | | `engine.block.isDropShadowEnabled(_:)` | Check whether the drop shadow is enabled | | `engine.block.setDropShadowColor(_:color:)` | Set the shadow color | | `engine.block.getDropShadowColor(_:)` | Get the shadow color (annotate return as `Color`) | | `engine.block.setDropShadowOffsetX(_:offsetX:)` | Set the horizontal shadow offset | | `engine.block.getDropShadowOffsetX(_:)` | Get the horizontal shadow offset | | `engine.block.setDropShadowOffsetY(_:offsetY:)` | Set the vertical shadow offset | | `engine.block.getDropShadowOffsetY(_:)` | Get the vertical shadow offset | | `engine.block.setDropShadowBlurRadiusX(_:blurRadiusX:)` | Set the horizontal blur radius | | `engine.block.getDropShadowBlurRadiusX(_:)` | Get the horizontal blur radius | | `engine.block.setDropShadowBlurRadiusY(_:blurRadiusY:)` | Set the vertical blur radius | | `engine.block.getDropShadowBlurRadiusY(_:)` | Get the vertical blur radius | ### Stroke Methods | Method | Description | | --- | --- | | `engine.block.supportsStroke(_:)` | Check whether a block supports strokes | | `engine.block.setStrokeEnabled(_:enabled:)` | Enable or disable the stroke | | `engine.block.isStrokeEnabled(_:)` | Check whether the stroke is enabled | | `engine.block.setStrokeWidth(_:width:)` | Set the stroke width | | `engine.block.getStrokeWidth(_:)` | Get the stroke width | | `engine.block.setStrokeColor(_:color:)` | Set the stroke color | | `engine.block.getStrokeColor(_:)` | Get the stroke color (annotate return as `Color`) | | `engine.block.setStrokeStyle(_:style:)` | Set the stroke line pattern | | `engine.block.getStrokeStyle(_:)` | Get the stroke line pattern | | `engine.block.setStrokePosition(_:position:)` | Set the stroke position relative to the text edge | | `engine.block.getStrokePosition(_:)` | Get the stroke position | ## Troubleshooting ### Drop Shadow Not Visible Ensure `setDropShadowEnabled(_:enabled:)` is called with `true`. Verify `supportsDropShadow(_:)` returns `true` for the text block, then adjust the shadow color, offset, and blur so the shadow is visible against the background. ### Stroke Not Visible Ensure `setStrokeEnabled(_:enabled:)` is called with `true` and the stroke width is greater than `0`. ### Stroke Too Thick or Thin Adjust the value passed to `setStrokeWidth(_:width:)` to control outline thickness. ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) — Configure fonts, colors, alignment, and other styling options. - [Using Strokes](https://img.ly/docs/cesdk/mac-catalyst/outlines/strokes-c2e621/) — Work with stroke controls beyond text outlines. - [Filters & Effects Overview](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/overview-299b15/) — Explore visual effects such as blur, duotone, LUTs, and chroma keying. - [Apply a Filter or Effect](https://img.ly/docs/cesdk/mac-catalyst/filters-and-effects/apply-2764e4/) — Apply effects to graphic 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: "Emojis" description: "Configure emoji rendering in CE.SDK text blocks with a dedicated emoji font for consistent display across platforms." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/emojis-510651/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Emojis](https://img.ly/docs/cesdk/mac-catalyst/text/emojis-510651/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-with-emojis/TextWithEmojis.swift reference-only import Foundation import IMGLYEngine @MainActor func textWithEmojis(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL let currentURI = try engine.editor.getSettingString("defaultEmojiFontFileUri") print("Current emoji font URI: \(currentURI)") try engine.editor.setSettingString( "defaultEmojiFontFileUri", value: baseURL.appendingPathComponent("emoji/NotoColorEmoji.ttf").absoluteString, ) // Pixel design units pair the font-size unit to pixels, so the `text/fontSize` // value below is 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: 1080) try engine.block.setHeight(page, value: 1080) try engine.block.appendChild(to: scene, child: page) try await engine.scene.zoom(to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40) let text = try engine.block.create(.text) try engine.block.replaceText(text, text: "Hello World! 🎉 🇩🇪 👨‍👩‍👧 👋🏽") try engine.block.setWidth(text, value: 900) try engine.block.setHeight(text, value: 200) try engine.block.appendChild(to: page, child: text) // Hero scaffolding: size the text and center it so emojis are legible at // thumbnail. The lesson itself does not require these calls, so they live // outside the highlight markers. try engine.block.setFloat(text, property: "text/fontSize", value: 64) try engine.block.setPositionX(text, value: 80) try engine.block.setPositionY(text, value: 480) // Snapshot the final scene as the guide's hero image. The label `"hero"` is // reserved and tells the promote script which baseline to convert to WebP. try await engine.captureGuide(page, label: "hero") } ``` Configure the emoji font that CE.SDK uses when rendering text blocks and add text content that includes emojis with the Swift Engine API. ![A text block rendering "Hello World!" alongside party popper, flag, family, and waving-hand emojis using the configured Noto Color Emoji font.](./assets/swift-based.hero.webp) > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-text-with-emojis) Emojis are Unicode characters representing pictographic symbols. They can be single code points (😀), multi-character sequences (flags like 🇩🇪), Zero-Width-Joiner combinations (👨‍👩‍👧), or skin-tone variants (👋🏽). Regular text fonts do not contain glyphs for these characters, so CE.SDK renders them with a separate emoji font that you configure through the engine's settings. ## Default Emoji Font CE.SDK ships with [Noto Color Emoji](https://github.com/googlefonts/noto-emoji) (~9.9 MB, PNG-based) loaded from `https://cdn.img.ly/assets/v4/emoji/NotoColorEmoji.ttf` by default. Because CE.SDK draws emojis from a dedicated font file rather than a system font, the same scene renders the same emoji glyphs on iPhone, iPad, and Mac. No configuration is needed for emoji rendering to work. Read the configured emoji-font override with `getSettingString(_:)`. It returns an empty string while CE.SDK is using its built-in default: ```swift highlight-textWithEmojis-getFont let currentURI = try engine.editor.getSettingString("defaultEmojiFontFileUri") print("Current emoji font URI: \(currentURI)") ``` ## Configuring the Emoji Font Point `defaultEmojiFontFileUri` at any accessible URL, CDN path, or bundle file with `setSettingString(_:value:)`. See the [Settings](https://img.ly/docs/cesdk/mac-catalyst/settings-970c98/) guide for the full settings API. ```swift highlight-textWithEmojis-setCustomFont try engine.editor.setSettingString( "defaultEmojiFontFileUri", value: baseURL.appendingPathComponent("emoji/NotoColorEmoji.ttf").absoluteString, ) ``` ## Adding Emojis to Text Blocks Create a `.text` block and pass emoji characters directly to `replaceText(_:text:)` as part of any UTF-8 string. CE.SDK detects emoji code points and renders them with the configured emoji font; everything else uses the block's regular font. ```swift highlight-textWithEmojis-addText let text = try engine.block.create(.text) try engine.block.replaceText(text, text: "Hello World! 🎉 🇩🇪 👨‍👩‍👧 👋🏽") try engine.block.setWidth(text, value: 900) try engine.block.setHeight(text, value: 200) try engine.block.appendChild(to: page, child: text) ``` ## The `forceSystemEmojis` Setting CE.SDK exposes a `forceSystemEmojis` boolean setting that defaults to `true`. When it is `true`, the engine treats every Unicode emoji code point as an emoji and renders it with `defaultEmojiFontFileUri`. When it is `false`, the engine first checks whether the text block's font contains a glyph for that code point and only falls back to the emoji font when it does not. In practice this rarely changes the rendered output, because most text fonts (Roboto, Open Sans, Inter, system fonts, etc.) do not ship emoji glyphs — so the fallback path lands on the emoji font either way. The setting is most useful when you load a custom font that does include its own emoji glyphs and you want those glyphs to render instead of Noto Color Emoji. ## Troubleshooting **Emojis look different than expected.** CE.SDK uses Noto Color Emoji by default. To switch to a different emoji style, point `defaultEmojiFontFileUri` at another emoji font. **Missing emojis.** Custom emoji fonts may not cover the full Unicode emoji range. Verify your font supports the code points you need before swapping the default. **Large initial download.** The default Noto Color Emoji font is roughly 9.9 MB. If you load it from a remote URL, consider bundling it with your app or prefetching it before the editor needs to render text. **Want system emojis instead.** CE.SDK always renders text through its own type setter for consistent layout across platforms, so it cannot reach into Apple's system emoji font (Apple Color Emoji) to draw glyphs. To get Apple's look you need to host an emoji font you have a license to use and point `defaultEmojiFontFileUri` at it. ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.editor.getSettingString("defaultEmojiFontFileUri")` | Get the current emoji font URI | | `engine.editor.setSettingString("defaultEmojiFontFileUri", value:)` | Set a custom emoji font URI | | `engine.block.create(.text)` | Create a text block | | `engine.block.replaceText(_:text:)` | Set the text content of a text block, including emoji characters | ### Properties | Property | Type | Description | | --- | --- | --- | | `defaultEmojiFontFileUri` | String | URI of the font file CE.SDK uses to render emoji characters | | `forceSystemEmojis` | Bool | When `true`, every Unicode emoji renders with `defaultEmojiFontFileUri`. When `false`, the engine first checks the text font for a matching glyph. Defaults to `true`. | ## Next Steps - [Text Overview](https://img.ly/docs/cesdk/mac-catalyst/text/overview-0bd620/) — Learn about text editing capabilities in CE.SDK - [Add Text](https://img.ly/docs/cesdk/mac-catalyst/text/add-4f5011/) — Create and add text blocks programmatically - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) — Apply fonts, colors, alignment, and other styling 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: "Text Enumerations" description: "Add bullet lists and numbered lists to text blocks in CE.SDK using per-paragraph list styles and nesting levels." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/enumerations-b5c1d2/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Text Enumerations](https://img.ly/docs/cesdk/mac-catalyst/text/enumerations-b5c1d2/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-enumerations/TextEnumerations.swift reference-only import IMGLYEngine @MainActor func textEnumerations(engine: Engine) async throws { let scene = try engine.scene.create() let text = try engine.block.create(.text) try engine.block.appendChild(to: scene, child: text) try engine.block.setWidthMode(text, mode: .auto) try engine.block.setHeightMode(text, mode: .auto) try engine.block.replaceText(text, text: "First item\nSecond item\nThird item") // Apply ordered list style to all paragraphs (paragraphIndex defaults to -1 = all) try engine.block.setTextListStyle(text, listStyle: .ordered) // Override the third paragraph (index 2) to unordered try engine.block.setTextListStyle(text, listStyle: .unordered, paragraphIndex: 2) // Set the second paragraph (index 1) to nesting level 1 (one indent deep) try engine.block.setTextListLevel(text, listLevel: 1, paragraphIndex: 1) // Read back the nesting level to confirm let level = try engine.block.getTextListLevel(text, paragraphIndex: 1) // level == 1 // Atomically set both list style and nesting level in one call // Sets paragraph 0 to ordered style at nesting level 0 (outermost) try engine.block.setTextListStyle(text, listStyle: .ordered, paragraphIndex: 0, listLevel: 0) // Get all paragraph indices in the text block let allIndices = try engine.block.getTextParagraphIndices(text) // allIndices == [0, 1, 2] // Get indices overlapping a specific character subrange let content = try engine.block.getString(text, property: "text/text") let subrange = content.startIndex ..< content.index(content.startIndex, offsetBy: 10) let rangeIndices = try engine.block.getTextParagraphIndices(text, in: subrange) // rangeIndices == [0] // Read back the list style and nesting level for each paragraph let styles = try allIndices.map { try engine.block.getTextListStyle(text, paragraphIndex: $0) } let levels = try allIndices.map { try engine.block.getTextListLevel(text, paragraphIndex: $0) } // styles == [.ordered, .ordered, .unordered] // levels == [0, 1, 0] _ = level _ = rangeIndices _ = styles _ = levels } ``` Apply bullet and numbered list styles to text blocks, control nesting levels, and query the list configuration of any paragraph. CE.SDK formats lists at the paragraph level. Each paragraph in a text block has an independent `ListStyle` (`.none`, `.unordered`, or `.ordered`) and a zero-based `listLevel` for visual nesting depth. A single API call can target one paragraph by index or all paragraphs at once using the default index of `-1`. ## Setup Create a text block with three paragraphs separated by newline characters. ```swift highlight-textEnumerations-setup let scene = try engine.scene.create() let text = try engine.block.create(.text) try engine.block.appendChild(to: scene, child: text) try engine.block.setWidthMode(text, mode: .auto) try engine.block.setHeightMode(text, mode: .auto) try engine.block.replaceText(text, text: "First item\nSecond item\nThird item") ``` ## Apply List Styles Use `engine.block.setTextListStyle(_:listStyle:paragraphIndex:)` to apply a `ListStyle` to one or all paragraphs. Passing no `paragraphIndex` (default `-1`) applies the style to every paragraph simultaneously. | Value | Renders as | |-------|-----------| | `.unordered` | Bullet marker (•) | | `.ordered` | Auto-incrementing number (1., 2., …) | | `.none` | Plain text — removes list formatting | ```swift highlight-textEnumerations-applyListStyles // Apply ordered list style to all paragraphs (paragraphIndex defaults to -1 = all) try engine.block.setTextListStyle(text, listStyle: .ordered) // Override the third paragraph (index 2) to unordered try engine.block.setTextListStyle(text, listStyle: .unordered, paragraphIndex: 2) ``` > **Note:** Write operations require the `"text/character"` scope to be enabled on the text block. ## Manage Nesting Levels Control the visual depth of list items using `engine.block.setTextListLevel(_:listLevel:paragraphIndex:)`. The level is zero-based: `0` is the outermost indent. Use `engine.block.getTextListLevel(_:paragraphIndex:)` to read the current depth. Nesting has no visual effect when the list style is `.none`. ```swift highlight-textEnumerations-manageNesting // Set the second paragraph (index 1) to nesting level 1 (one indent deep) try engine.block.setTextListLevel(text, listLevel: 1, paragraphIndex: 1) // Read back the nesting level to confirm let level = try engine.block.getTextListLevel(text, paragraphIndex: 1) // level == 1 ``` ## Atomic Style and Level Assignment Pass the optional `listLevel` parameter to `setTextListStyle` to set both the style and nesting level in a single call. ```swift highlight-textEnumerations-atomic // Atomically set both list style and nesting level in one call // Sets paragraph 0 to ordered style at nesting level 0 (outermost) try engine.block.setTextListStyle(text, listStyle: .ordered, paragraphIndex: 0, listLevel: 0) ``` ## Resolve Paragraph Indices Use `engine.block.getTextParagraphIndices(_:in:)` to find which paragraph indices overlap a text range. Pass `nil` (the default) to retrieve all indices. This is the right tool before targeted per-paragraph operations when you only know a character position—for example, after calling `engine.block.getTextCursorRange()` to get the current selection. ```swift highlight-textEnumerations-paragraphIndices // Get all paragraph indices in the text block let allIndices = try engine.block.getTextParagraphIndices(text) // allIndices == [0, 1, 2] // Get indices overlapping a specific character subrange let content = try engine.block.getString(text, property: "text/text") let subrange = content.startIndex ..< content.index(content.startIndex, offsetBy: 10) let rangeIndices = try engine.block.getTextParagraphIndices(text, in: subrange) // rangeIndices == [0] ``` ## Query List Styles Read the list style and nesting level of each paragraph using `getTextListStyle` and `getTextListLevel`. Both getters require a non-negative `paragraphIndex`. ```swift highlight-textEnumerations-queryListStyles // Read back the list style and nesting level for each paragraph let styles = try allIndices.map { try engine.block.getTextListStyle(text, paragraphIndex: $0) } let levels = try allIndices.map { try engine.block.getTextListLevel(text, paragraphIndex: $0) } // styles == [.ordered, .ordered, .unordered] // levels == [0, 1, 0] ``` ## Full Code ```swift highlight-textEnumerations import IMGLYEngine @MainActor func textEnumerations(engine: Engine) async throws { let scene = try engine.scene.create() let text = try engine.block.create(.text) try engine.block.appendChild(to: scene, child: text) try engine.block.setWidthMode(text, mode: .auto) try engine.block.setHeightMode(text, mode: .auto) try engine.block.replaceText(text, text: "First item\nSecond item\nThird item") // Apply ordered list style to all paragraphs (paragraphIndex defaults to -1 = all) try engine.block.setTextListStyle(text, listStyle: .ordered) // Override the third paragraph (index 2) to unordered try engine.block.setTextListStyle(text, listStyle: .unordered, paragraphIndex: 2) // Set the second paragraph (index 1) to nesting level 1 (one indent deep) try engine.block.setTextListLevel(text, listLevel: 1, paragraphIndex: 1) // Read back the nesting level to confirm let level = try engine.block.getTextListLevel(text, paragraphIndex: 1) // level == 1 // Atomically set both list style and nesting level in one call // Sets paragraph 0 to ordered style at nesting level 0 (outermost) try engine.block.setTextListStyle(text, listStyle: .ordered, paragraphIndex: 0, listLevel: 0) // Get all paragraph indices in the text block let allIndices = try engine.block.getTextParagraphIndices(text) // allIndices == [0, 1, 2] // Get indices overlapping a specific character subrange let content = try engine.block.getString(text, property: "text/text") let subrange = content.startIndex ..< content.index(content.startIndex, offsetBy: 10) let rangeIndices = try engine.block.getTextParagraphIndices(text, in: subrange) // rangeIndices == [0] // Read back the list style and nesting level for each paragraph let styles = try allIndices.map { try engine.block.getTextListStyle(text, paragraphIndex: $0) } let levels = try allIndices.map { try engine.block.getTextListLevel(text, paragraphIndex: $0) } // styles == [.ordered, .ordered, .unordered] // levels == [0, 1, 0] _ = level _ = rangeIndices _ = styles _ = levels } ``` ## Next Steps [Text Decorations](https://img.ly/docs/cesdk/mac-catalyst/text/decorations-d3c0a1/) [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) [Edit Text](https://img.ly/docs/cesdk/mac-catalyst/text/edit-c5106b/) --- ## 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 and Language Support" description: "Create designs that work across different languages and writing systems with RTL text, complex scripts, and multilingual font support." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/language-support-a0f010/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Language Support](https://img.ly/docs/cesdk/mac-catalyst/text/language-support-a0f010/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-language-support/LanguageSupport.swift reference-only import Foundation import IMGLYEngine @MainActor func languageSupport(engine: Engine) async throws { let baseURL = try engine.guidesBaseURL // Scaffolding: create a scene + page so the rest of the example has somewhere // to attach blocks. The reader is expected to have their own scene context. let scene = try engine.scene.create(designUnit: .px) 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: 800) let roboto = Typeface( name: "Roboto", 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 notoArabic = Typeface( name: "Noto Sans Arabic", fonts: [ Font( uri: baseURL.appendingPathComponent("fonts/font-6.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), ], ) let notoKorean = Typeface( name: "Noto Sans KR", fonts: [ Font( uri: baseURL.appendingPathComponent("fonts/font-30.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), ], ) let latinText = try engine.block.create(.text) try engine.block.replaceText(latinText, text: "Multilingual typography") try engine.block.appendChild(to: page, child: latinText) try engine.block.setPositionX(latinText, value: 50) try engine.block.setPositionY(latinText, value: 30) try engine.block.setWidth(latinText, value: 700) try engine.block.setHeight(latinText, value: 80) try engine.block.setTextFontSize(latinText, fontSize: 26) try engine.block.setTypeface(latinText, typeface: roboto) let mixed = "Mix Roboto Bold and Regular" let bold = try engine.block.create(.text) try engine.block.replaceText(bold, text: mixed) try engine.block.appendChild(to: page, child: bold) try engine.block.setPositionX(bold, value: 50) try engine.block.setPositionY(bold, value: 140) try engine.block.setWidth(bold, value: 700) try engine.block.setHeight(bold, value: 50) try engine.block.setTextFontSize(bold, fontSize: 20) try engine.block.setTypeface(bold, typeface: roboto, in: mixed.range(of: "Roboto Bold")!) let koreanText = try engine.block.create(.text) try engine.block.replaceText(koreanText, text: "안녕하세요 세계") try engine.block.appendChild(to: page, child: koreanText) try engine.block.setPositionX(koreanText, value: 50) try engine.block.setPositionY(koreanText, value: 310) try engine.block.setWidth(koreanText, value: 700) try engine.block.setHeight(koreanText, value: 80) try engine.block.setTextFontSize(koreanText, fontSize: 28) try engine.block.setTypeface(koreanText, typeface: notoKorean) try engine.block.setTextHorizontalAlignment(latinText, alignment: .auto) let arabicText = try engine.block.create(.text) try engine.block.replaceText(arabicText, text: "مرحبا بالعالم") try engine.block.appendChild(to: page, child: arabicText) try engine.block.setPositionX(arabicText, value: 50) try engine.block.setPositionY(arabicText, value: 210) try engine.block.setWidth(arabicText, value: 700) try engine.block.setHeight(arabicText, value: 80) try engine.block.setTextFontSize(arabicText, fontSize: 28) try engine.block.setTypeface(arabicText, typeface: notoArabic) try engine.block.setTextHorizontalAlignment(arabicText, alignment: .right) let effective = try engine.block.getTextEffectiveHorizontalAlignment(arabicText) print("Effective alignment is right:", effective == .right) let mixedText = try engine.block.create(.text) try engine.block.replaceText(mixedText, text: "Heading\nSubtitle\nBody copy") try engine.block.appendChild(to: page, child: mixedText) try engine.block.setPositionX(mixedText, value: 50) try engine.block.setPositionY(mixedText, value: 410) try engine.block.setWidth(mixedText, value: 700) try engine.block.setHeight(mixedText, value: 220) try engine.block.setTextFontSize(mixedText, fontSize: 22) // Block-level default — every paragraph without an override inherits this. try engine.block.setTextHorizontalAlignment(mixedText, alignment: .left) // Override the second paragraph (index 1) only. try engine.block.setTextHorizontalAlignment(mixedText, alignment: .right, paragraphIndex: 1) let para0 = try engine.block.getTextHorizontalAlignment(mixedText, paragraphIndex: 0) let para1 = try engine.block.getTextHorizontalAlignment(mixedText, paragraphIndex: 1) let blockDefault = try engine.block.getTextHorizontalAlignment(mixedText) print("paragraph 0 inherits block-level:", para0 == nil) print("paragraph 1 override:", para1 == .right ? "right" : "other") print("block-level default:", blockDefault == .left ? "left" : "other") try engine.block.setTextHorizontalAlignment(mixedText, alignment: nil, paragraphIndex: 1) let allIndices = try engine.block.getTextParagraphIndices(mixedText) print("Paragraph indices:", allIndices) try engine.variable.set(key: "greeting", value: "Hello world") let dynamicText = try engine.block.create(.text) try engine.block.replaceText(dynamicText, text: "{{greeting}}") try engine.block.appendChild(to: page, child: dynamicText) try engine.block.setPositionX(dynamicText, value: 50) try engine.block.setPositionY(dynamicText, value: 660) try engine.block.setWidth(dynamicText, value: 700) try engine.block.setHeight(dynamicText, value: 80) try engine.block.setTextFontSize(dynamicText, fontSize: 22) try engine.block.setTypeface(dynamicText, typeface: roboto) // Update the variable later to swap the rendered content to a different // language — the existing block re-renders with the new value. try engine.variable.set(key: "greeting", value: "Bonjour le monde") try await engine.captureGuide(page, label: "hero") } ``` Configure typefaces, manage right-to-left text, and bind multilingual content to variables using the Block API. ![A scene with Latin, Arabic, and Korean text demonstrating typeface application, range-targeted typefaces, right-to-left Arabic rendered with Noto Sans Arabic, Korean rendered with Noto Sans KR, and a variable substituted to French.](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-text-language-support) The engine handles text shaping, bidirectional layout, and script-specific rendering automatically — every Unicode character, complex script ligatures, and mixed LTR/RTL content render without additional configuration. The Block API exposes the knobs that drive those defaults: typefaces, block- and paragraph-level alignment, and variables for dynamic content. This guide covers programmatic font configuration, automatic right-to-left detection, paragraph-level alignment overrides, and variable bindings for multilingual content. ## Programmatic Font and Typeface Management A `Typeface` bundles one or more `Font` files under a shared family name. Each `Font` carries a URL, sub-family label, weight, and style — the engine picks the right file when text formatting changes the rendered weight or style. ### Configuring Typefaces for Language Support Build a dedicated `Typeface` for each script your design renders. Fonts can come from your app bundle, a remote URL, or — as the example shows — the bundled asset source. ```swift highlight-langSupport-typeface let roboto = Typeface( name: "Roboto", 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, ), ], ) ``` ```swift highlight-langSupport-scriptTypefaces let notoArabic = Typeface( name: "Noto Sans Arabic", fonts: [ Font( uri: baseURL.appendingPathComponent("fonts/font-6.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), ], ) let notoKorean = Typeface( name: "Noto Sans KR", fonts: [ Font( uri: baseURL.appendingPathComponent("fonts/font-30.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), ], ) ``` The example defines three typefaces: - **Roboto** — a Latin-script family with regular and bold weights for body text and emphasis. - **Noto Sans Arabic** — carries the OpenType tables the engine relies on for contextual shaping. Also covers Persian and Urdu. - **Noto Sans KR** — supports Korean Hangul. Each `Font` includes: - `uri` — path to the font file (TTF, OTF, or WOFF2). - `subFamily` — a human-readable label (`"Regular"`, `"Bold Italic"`, …) used by the asset-source schema for naming. - `weight` — `.thin` (100) through `.heavy` (900); the engine matches `weight` against the text's current formatting when picking a file. - `style` — `.normal` or `.italic`. In production, ship these font files alongside your app or load them from a controlled URL — comprehensive Unicode coverage avoids missing-glyph rendering for the scripts your users care about. ### Applying Typefaces to a Block Use `setTypeface(_:typeface:)` to apply a typeface to a whole text block. Existing run-level formatting is preserved where the new typeface supports it; runs that don't have a matching font fall back to the typeface's default. ```swift highlight-langSupport-applyTypeface let latinText = try engine.block.create(.text) try engine.block.replaceText(latinText, text: "Multilingual typography") try engine.block.appendChild(to: page, child: latinText) try engine.block.setPositionX(latinText, value: 50) try engine.block.setPositionY(latinText, value: 30) try engine.block.setWidth(latinText, value: 700) try engine.block.setHeight(latinText, value: 80) try engine.block.setTextFontSize(latinText, fontSize: 26) try engine.block.setTypeface(latinText, typeface: roboto) ``` The same setter accepts a string subrange to apply a typeface to only part of the text — for mixing scripts within a single block, for example. ```swift highlight-langSupport-rangeTypeface let mixed = "Mix Roboto Bold and Regular" let bold = try engine.block.create(.text) try engine.block.replaceText(bold, text: mixed) try engine.block.appendChild(to: page, child: bold) try engine.block.setPositionX(bold, value: 50) try engine.block.setPositionY(bold, value: 140) try engine.block.setWidth(bold, value: 700) try engine.block.setHeight(bold, value: 50) try engine.block.setTextFontSize(bold, fontSize: 20) try engine.block.setTypeface(bold, typeface: roboto, in: mixed.range(of: "Roboto Bold")!) ``` The setter works for any typeface — applying Noto Sans KR to a Korean block follows the same pattern: ```swift highlight-langSupport-wideScript let koreanText = try engine.block.create(.text) try engine.block.replaceText(koreanText, text: "안녕하세요 세계") try engine.block.appendChild(to: page, child: koreanText) try engine.block.setPositionX(koreanText, value: 50) try engine.block.setPositionY(koreanText, value: 310) try engine.block.setWidth(koreanText, value: 700) try engine.block.setHeight(koreanText, value: 80) try engine.block.setTextFontSize(koreanText, fontSize: 28) try engine.block.setTypeface(koreanText, typeface: notoKorean) ``` For read-back, `getTypeface(_:)` returns the block's default typeface and `getTypefaces(_:in:)` returns the typefaces of every text run in the optional range. Both are documented in the API Reference below; the runnable example doesn't call either. ## Working with Right-to-Left (RTL) Text ### Understanding Automatic RTL Detection The engine implements the Unicode Bidirectional Algorithm (UAX #9) and determines text direction from the Unicode bidirectional class of the characters in the block. Strong RTL characters from Arabic, Hebrew, Persian, and Urdu establish a right-to-left flow; embedded LTR words like English brand names are positioned correctly without manual intervention. `HorizontalTextAlignment.auto` defers the alignment decision to that analysis — RTL scripts align right, LTR scripts align left, and the same template renders correctly across writing systems without an alignment branch in your code: ```swift highlight-langSupport-autoAlignment try engine.block.setTextHorizontalAlignment(latinText, alignment: .auto) ``` This automatic detection works for: - Arabic — including Persian and Urdu variants - Hebrew — modern and Biblical Hebrew - Mixed content — English or other LTR text within RTL paragraphs ### Text Alignment for RTL Languages `HorizontalTextAlignment` has four cases: | Case | Behavior | | --- | --- | | `.left` | Always align text to the left. | | `.right` | Always align text to the right. | | `.center` | Center-align text. | | `.auto` | Resolve direction from the script — RTL scripts align right, LTR scripts align left. | `setTextHorizontalAlignment(_:alignment:)` with the default negative `paragraphIndex` sets a block-level value. The example below creates an Arabic text block, applies the Noto Sans Arabic typeface, sets `.right` explicitly, and reads back the effective alignment: ```swift highlight-langSupport-effectiveAlignment let arabicText = try engine.block.create(.text) try engine.block.replaceText(arabicText, text: "مرحبا بالعالم") try engine.block.appendChild(to: page, child: arabicText) try engine.block.setPositionX(arabicText, value: 50) try engine.block.setPositionY(arabicText, value: 210) try engine.block.setWidth(arabicText, value: 700) try engine.block.setHeight(arabicText, value: 80) try engine.block.setTextFontSize(arabicText, fontSize: 28) try engine.block.setTypeface(arabicText, typeface: notoArabic) try engine.block.setTextHorizontalAlignment(arabicText, alignment: .right) let effective = try engine.block.getTextEffectiveHorizontalAlignment(arabicText) print("Effective alignment is right:", effective == .right) ``` `getTextEffectiveHorizontalAlignment(_:)` returns the resolved direction the engine uses to lay out the block. When the alignment is `.auto`, the getter returns `.left` or `.right` based on the script of the first logical run — never `.auto` — so a UI control can display the concrete direction the engine picked. ## Paragraph-Level Alignment Overrides Pass a non-negative `paragraphIndex` to override the alignment of a single paragraph. The block-level default still applies to every paragraph that doesn't carry an override. ```swift highlight-langSupport-paragraphAlignment let mixedText = try engine.block.create(.text) try engine.block.replaceText(mixedText, text: "Heading\nSubtitle\nBody copy") try engine.block.appendChild(to: page, child: mixedText) try engine.block.setPositionX(mixedText, value: 50) try engine.block.setPositionY(mixedText, value: 410) try engine.block.setWidth(mixedText, value: 700) try engine.block.setHeight(mixedText, value: 220) try engine.block.setTextFontSize(mixedText, fontSize: 22) // Block-level default — every paragraph without an override inherits this. try engine.block.setTextHorizontalAlignment(mixedText, alignment: .left) // Override the second paragraph (index 1) only. try engine.block.setTextHorizontalAlignment(mixedText, alignment: .right, paragraphIndex: 1) ``` Read back an override with `getTextHorizontalAlignment(_:paragraphIndex:)`. It returns `nil` when the paragraph inherits the block-level value, and the override otherwise. Passing a negative `paragraphIndex` returns the block-level value itself. ```swift highlight-langSupport-readbackAlignment let para0 = try engine.block.getTextHorizontalAlignment(mixedText, paragraphIndex: 0) let para1 = try engine.block.getTextHorizontalAlignment(mixedText, paragraphIndex: 1) let blockDefault = try engine.block.getTextHorizontalAlignment(mixedText) print("paragraph 0 inherits block-level:", para0 == nil) print("paragraph 1 override:", para1 == .right ? "right" : "other") print("block-level default:", blockDefault == .left ? "left" : "other") ``` Clear an override by passing `nil` as the alignment. The paragraph reverts to the block-level default. ```swift highlight-langSupport-clearOverride try engine.block.setTextHorizontalAlignment(mixedText, alignment: nil, paragraphIndex: 1) ``` Use `getTextParagraphIndices(_:in:)` to discover valid paragraph indices before querying overrides. Passing an optional string subrange restricts the result to paragraphs that overlap the range. ```swift highlight-langSupport-paragraphIndices let allIndices = try engine.block.getTextParagraphIndices(mixedText) print("Paragraph indices:", allIndices) ``` > **Note:** Calling `setTextHorizontalAlignment` with a negative `paragraphIndex` and a non-`nil` alignment clears every paragraph-level override at once. If your app applies overrides and then changes the block-level alignment, re-apply the paragraph overrides afterward. ## Complex Script Support The text engine ships script-aware shaping and applies the right OpenType features automatically when the typeface includes the necessary tables. No additional configuration is required. ### Arabic Script Features When the configured typeface ships the necessary OpenType tables, the engine's HarfBuzz shaper applies Arabic-specific rendering automatically: - Contextual letter forms — letters change shape based on position (initial, medial, final, isolated) when the font has the `init` / `medi` / `fina` features - Required ligatures — mandatory character combinations render as single glyphs through the `liga` feature - Diacritical-mark positioning — tashkeel marks anchor correctly via `mark` and `mkmk` tables ### Other Complex Scripts The same shaping pipeline covers other writing systems when the typeface ships the required tables: - Devanagari (Hindi, Sanskrit) — conjunct formations and half-forms - Thai — vowel and tone mark positioning above and below base characters - Japanese — Kanji, hiragana, and katakana rendering - Southeast Asian scripts — Khmer subscripts, Myanmar ligatures ## Creating Multilingual Design Templates ### Using Variables for Multilingual Content Bind dynamic content to a variable instead of hardcoding the text on the block. Updating the variable swaps the rendered content without rebuilding the block, so the same template can render in any language. ```swift highlight-langSupport-variables try engine.variable.set(key: "greeting", value: "Hello world") let dynamicText = try engine.block.create(.text) try engine.block.replaceText(dynamicText, text: "{{greeting}}") try engine.block.appendChild(to: page, child: dynamicText) try engine.block.setPositionX(dynamicText, value: 50) try engine.block.setPositionY(dynamicText, value: 660) try engine.block.setWidth(dynamicText, value: 700) try engine.block.setHeight(dynamicText, value: 80) try engine.block.setTextFontSize(dynamicText, fontSize: 22) try engine.block.setTypeface(dynamicText, typeface: roboto) // Update the variable later to swap the rendered content to a different // language — the existing block re-renders with the new value. try engine.variable.set(key: "greeting", value: "Bonjour le monde") ``` `engine.variable.set(key:value:)` creates or updates a variable. `findAll()` lists every variable currently registered, and `remove(key:)` destroys a variable. Benefits of using variables: - **Dynamic language switching** — update variable values to change content language at runtime. - **Consistent formatting** — text styling and layout remain stable across languages. - **Template reusability** — one template works for multiple language markets. Variables are particularly useful for: - **Localized marketing materials** — same design, different language content. - **A/B testing** — test messaging across languages. - **Regional campaigns** — deploy region-specific content from a single template. ### Template Design Considerations Account for these factors when designing multilingual templates: - Text expansion — some languages require 30–50% more space than English - Direction changes — RTL layouts may need mirrored designs - Font availability — make sure each typeface covers every target script - Line height — scripts with diacritics may need additional vertical space Test templates with representative content in every target language to verify the layout works correctly. ## Font Fallback and Character Coverage ### Font Selection Priority When a glyph is missing from the configured typeface's primary font, the engine searches in this order: 1. The exact `weight` + `style` match in the typeface's `fonts` array 2. A similar `weight` (mapped via the engine's weight-replacement table) within the same typeface 3. The `fallbackFontUri` engine setting if one is configured 4. A Noto font fetched from the IMG.LY CDN for the glyph's script, while the `useSystemFontFallback` setting is `true` (the default) 5. The `defaultFontFileUri` engine setting 6. The `.notdef` glyph (rendered as an empty box) when no font contains the character The CDN Noto set covers most of Unicode out of the box, so missing glyphs auto-fill without extra configuration — the engine does not consult the device's own OS fonts, only the CDN set. Emoji code points use the separate `defaultEmojiFontFileUri` setting when configured. ### Ensuring Complete Character Coverage Use typefaces that cover every script you ship. Comprehensive Unicode coverage avoids missing-glyph rendering and reduces visual inconsistency caused by automatic fallback to a different font family. - Use comprehensive typefaces — the Noto family covers most Unicode scripts - Test with target-language samples — verify fonts include the required scripts before deployment - Configure fallback stacks — define multiple typefaces for comprehensive coverage - Check font formats — ensure fonts include OpenType layout tables for complex scripts ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.setTypeface(_:typeface:in:)` | Apply a typeface to the whole text block or a string subrange. | | `engine.block.getTypeface(_:)` | Get the block-level typeface. | | `engine.block.getTypefaces(_:in:)` | Get the typefaces of every text run in an optional subrange. | | `engine.block.setTextHorizontalAlignment(_:alignment:paragraphIndex:)` | Set the alignment for one paragraph (`paragraphIndex >= 0`) or the whole block (negative index). Pass a `nil` alignment to clear a paragraph-level override. | | `engine.block.getTextHorizontalAlignment(_:paragraphIndex:)` | Get the paragraph-level override; returns `nil` when the paragraph inherits the block-level value. | | `engine.block.getTextEffectiveHorizontalAlignment(_:)` | Get the resolved alignment after layout — explicit cases return the stored value, `.auto` resolves to `.left` or `.right` based on the first logical run. | | `engine.block.getTextParagraphIndices(_:in:)` | Get the 0-based paragraph indices overlapping a grapheme range. | | `engine.block.replaceText(_:text:in:)` | Replace the text content of a block or a string subrange. Variable tokens written as `{{name}}` substitute at render time. | | `engine.block.setTextFontSize(_:fontSize:in:)` | Set the font size of the whole text block or a string subrange. | | `engine.block.getTextFontSizes(_:in:)` | Get the ordered unique list of font sizes in an optional subrange. | | `engine.variable.set(key:value:)` | Create or update a text variable for substitution at render time. | | `engine.variable.findAll()` | List every registered variable. | | `engine.variable.remove(key:)` | Destroy a variable. | ### Types | Type | Description | | --- | --- | | `Typeface` | Family of fonts sharing a `name`; carries an array of `Font` files. | | `Font` | Single font file with a `uri`, `subFamily` label, `weight`, and `style`. | | `HorizontalTextAlignment` | `.left`, `.right`, `.center`, `.auto`. | ## Troubleshooting ### Text Displays as Squares or Question Marks The selected font is missing glyphs for the script you're rendering. Switch to a typeface with comprehensive Unicode coverage (the Noto family is a good starting point), verify the font file format (TTF, OTF, or WOFF2), and confirm the font URL resolves. ### RTL Text Renders Left-to-Right Set the block's alignment to either `.auto` or `.right` via `setTextHorizontalAlignment(_:alignment:)`. After layout, call `getTextEffectiveHorizontalAlignment(_:)` to confirm the resolved direction. If it returns `.left` for text you expect to flow RTL, the string probably begins with neutral characters — leading punctuation or numbers can flip the resolved direction. ### Ligatures or Diacritics Display Incorrectly Use a font designed for the target script. The font must include GSUB/GPOS OpenType tables for the engine to apply contextual forms, ligatures, and mark positioning. ### Mixed-Direction Text Layout Issues The engine resolves bidirectional layout from the Unicode character properties of the text. If a sentence with both LTR and RTL content lays out incorrectly, the source string may need Unicode directional formatting characters — RLM (U+200F) for right-to-left or LRM (U+200E) for left-to-right — at the boundaries where the direction switches. ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) — Apply fonts, colors, alignment, and other styling options to customize text appearance. - [Text Overview](https://img.ly/docs/cesdk/mac-catalyst/text/overview-0bd620/) — Learn about text editing capabilities in CE.SDK. - [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 during design generation. - [Edit Text](https://img.ly/docs/cesdk/mac-catalyst/text/edit-c5106b/) — Edit text content directly on the canvas or through the properties panel. --- ## 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, style, and customize text layers in your design using CE.SDK’s flexible text editing tools." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/overview-0bd620/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Overview](https://img.ly/docs/cesdk/mac-catalyst/text/overview-0bd620/) --- In CreativeEditor SDK (CE.SDK), a *text element* is an editable, stylable block that you can add to your design. Whether you're creating marketing graphics, videos, social media posts, or multilingual layouts, text plays a vital role in conveying information and enhancing your visuals. You can fully manipulate text elements using both the user interface and programmatic APIs, giving you maximum flexibility to control how text behaves and appears. Additionally, text can be animated to bring motion to your designs. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) --- ## 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 Styling" description: "Apply fonts, colors, alignment, and other styling options to customize text appearance." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-properties/TextProperties.swift reference-only import Foundation import IMGLYEngine @MainActor func textProperties(engine: Engine) async throws { // Demo scaffolding: a Pixel-unit page so `text/fontSize` literals interpret // as pixels and the captured exports show the rendered output at its // intended scale. 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: 300) try engine.block.appendChild(to: scene, child: page) let text = try engine.block.create(.text) try engine.block.appendChild(to: page, child: text) try engine.block.setWidthMode(text, mode: .auto) try engine.block.setHeightMode(text, mode: .auto) try engine.block.replaceText(text, text: "Hello World") // Add a "!" at the end of the text try engine.block.replaceText(text, text: "!", in: "Hello World".endIndex ..< "Hello World".endIndex) // Replace "World" with "CE.SDK" try engine.block.replaceText(text, text: "CE.SDK", in: "Hello World".range(of: "World")!) try engine.block.setTextFontSize(text, fontSize: 80) try engine.block.setPositionX(text, value: 230) try engine.block.setPositionY(text, value: 100) try await engine.scene.zoom(to: text, paddingLeft: 100, paddingTop: 100, paddingRight: 100, paddingBottom: 100) // Remove the "Hello " try engine.block.removeText(text, from: "Hello CE.SDK".range(of: "Hello ")!) try engine.block.setTextColor(text, color: .rgba(r: 1, g: 1, b: 0)) try engine.block.setTextColor(text, color: .rgba(r: 0, g: 0, b: 0), in: "CE.SDK".range(of: "E.SDK")!) let allColors = try engine.block.getTextColors(text) print("All unique colors: \(allColors)") let colorsInRange = try engine.block.getTextColors(text, in: "CE.SDK!".range(of: "E.SDK!")!) print("Colors in \"E.SDK!\": \(colorsInRange)") try engine.block.setBackgroundColorEnabled(text, enabled: true) let currentBackgroundColor = try engine.block.getBackgroundColor(text) print("Current background color: \(currentBackgroundColor)") try engine.block.setBackgroundColor(text, r: 0.0, g: 0.0, b: 1.0, a: 1.0) try engine.block.setFloat(text, property: "backgroundColor/paddingLeft", value: 8) try engine.block.setFloat(text, property: "backgroundColor/paddingTop", value: 8) try engine.block.setFloat(text, property: "backgroundColor/paddingRight", value: 8) try engine.block.setFloat(text, property: "backgroundColor/paddingBottom", value: 8) try engine.block.setFloat(text, property: "backgroundColor/cornerRadius", value: 12) // Most-evolved positive visual state — captured before setInAnimation makes // the text invisible at t=0 and before setFont resets the per-range colors. try await engine.captureGuide(page, label: "hero") let animation = try engine.block.createAnimation(AnimationType.slide) try engine.block.setEnum(animation, property: "textAnimationWritingStyle", value: "Block") try engine.block.setInAnimation(text, animation: animation) try engine.block.setOutAnimation(text, animation: animation) try engine.block.setTextCase(text, textCase: .titlecase) let textCases = try engine.block.getTextCases(text) print("Text cases: \(textCases)") let baseURL = try engine.guidesBaseURL let typeface = Typeface( name: "Roboto", fonts: [ Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-BoldItalic.ttf"), subFamily: "Bold Italic", weight: .bold, style: .italic, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Italic.ttf"), subFamily: "Italic", weight: .normal, style: .italic, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), ], ) try engine.block.setFont(text, fontFileURL: typeface.fonts[3].uri, typeface: typeface) try engine.block.setTypeface(text, typeface: typeface, in: "CE.SDK".range(of: "E.SDK")!) try engine.block.setTypeface(text, typeface: typeface) let currentDefaultTypeface = try engine.block.getTypeface(text) print("Default typeface: \(currentDefaultTypeface.name)") let currentTypefaces = try engine.block.getTypefaces(text) let currentTypefacesOfRange = try engine.block.getTypefaces(text, in: "CE.SDK".range(of: "E.SDK")!) print("Typefaces across the block: \(currentTypefaces.map(\.name))") print("Typefaces in \"E.SDK\": \(currentTypefacesOfRange.map(\.name))") if try engine.block.canToggleBoldFont(text) { try engine.block.toggleBoldFont(text) } if try engine.block.canToggleBoldFont(text, in: "CE.SDK".range(of: "E.SDK")!) { try engine.block.toggleBoldFont(text, in: "CE.SDK".range(of: "E.SDK")!) } if try engine.block.canToggleItalicFont(text) { try engine.block.toggleItalicFont(text) } if try engine.block.canToggleItalicFont(text, in: "CE.SDK".range(of: "E.SDK")!) { try engine.block.toggleItalicFont(text, in: "CE.SDK".range(of: "E.SDK")!) } try engine.block.setTextFontWeight(text, fontWeight: .bold) let fontWeights = try engine.block.getTextFontWeights(text) print("Font weights: \(fontWeights)") try engine.block.setTextFontStyle(text, fontStyle: .italic) let fontStyles = try engine.block.getTextFontStyles(text) print("Font styles: \(fontStyles)") } ``` Style text blocks programmatically with colors, backgrounds, typefaces, and formatting. ![Two-color text on a rounded blue background demonstrating per-range colors, padding, and corner radius.](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-text-properties) CE.SDK exposes text styling through the Block API. Edit text content, apply colors and backgrounds, manage typefaces, and control case, weight, and style for an entire text block or for individual character ranges. Ranges are described with native Swift `Range` values. ## Editing Text Content Use `engine.block.replaceText(_:text:in:)` and `engine.block.removeText(_:from:)` to replace, insert, or remove text. The `in` and `from` arguments are optional Swift string ranges — passing `nil` applies the operation to the entire string. The snippets below compute ranges with `String.range(of:)` against a literal that matches the block's current text — `"Hello World".range(of: "World")` works only because the block currently holds `"Hello World"`. When you tweak the example, update each literal to match the live text before computing the range. Replacing the whole string is the common case: ```swift highlight-replaceText try engine.block.replaceText(text, text: "Hello World") ``` Pass an empty range to insert at its lower bound. The example appends a `"!"` after the existing text: ```swift highlight-replaceText-single-index // Add a "!" at the end of the text try engine.block.replaceText(text, text: "!", in: "Hello World".endIndex ..< "Hello World".endIndex) ``` Use `String.range(of:)` to locate a substring and replace only that range. Here `"World"` becomes `"CE.SDK"`: ```swift highlight-replaceText-range // Replace "World" with "CE.SDK" try engine.block.replaceText(text, text: "CE.SDK", in: "Hello World".range(of: "World")!) ``` `removeText(_:from:)` mirrors the same shape. Pass an explicit range to remove a substring, or pass `nil` to clear the whole text: ```swift highlight-removeText // Remove the "Hello " try engine.block.removeText(text, from: "Hello CE.SDK".range(of: "Hello ")!) ``` ## Text Colors Apply colors to the entire text block or to a specific range with `engine.block.setTextColor(_:color:in:)`. Use `Color.rgba(r:g:b:a:)` for RGBA colors or `Color.spot(name:tint:)` for spot colors. Color the whole string first: ```swift highlight-setTextColor try engine.block.setTextColor(text, color: .rgba(r: 1, g: 1, b: 0)) ``` Then override the color on a specific range. The example below leaves the leading `"C"` and the trailing `"!"` yellow, but renders the middle `"E.SDK"` in black: ```swift highlight-setTextColor-range try engine.block.setTextColor(text, color: .rgba(r: 0, g: 0, b: 0), in: "CE.SDK".range(of: "E.SDK")!) ``` `engine.block.getTextColors(_:in:)` returns the ordered list of unique colors in the requested range. For the full text the result is `[yellow, black]`, since yellow appears first: ```swift highlight-getTextColors let allColors = try engine.block.getTextColors(text) print("All unique colors: \(allColors)") ``` Querying the `"E.SDK!"` range returns `[black, yellow]` — the range starts in the black middle and ends on the yellow `!`, so black appears first: ```swift highlight-getTextColors-range let colorsInRange = try engine.block.getTextColors(text, in: "CE.SDK!".range(of: "E.SDK!")!) print("Colors in \"E.SDK!\": \(colorsInRange)") ``` ## Text Backgrounds Enable the background, then customize the color, padding, and corner radius. Enable the background with `engine.block.setBackgroundColorEnabled(_:enabled:)`: ```swift highlight-backgroundColor-enabled try engine.block.setBackgroundColorEnabled(text, enabled: true) ``` Read the current background color with `engine.block.getBackgroundColor(_:)` and change it with `engine.block.setBackgroundColor(_:r:g:b:a:)`: ```swift highlight-backgroundColor-get-set let currentBackgroundColor = try engine.block.getBackgroundColor(text) print("Current background color: \(currentBackgroundColor)") try engine.block.setBackgroundColor(text, r: 0.0, g: 0.0, b: 1.0, a: 1.0) ``` Padding and corner radius are exposed as `backgroundColor/*` block properties. Adjust each padding side independently using `engine.block.setFloat(_:property:value:)` and the `backgroundColor/paddingLeft`, `backgroundColor/paddingRight`, `backgroundColor/paddingTop`, and `backgroundColor/paddingBottom` properties: ```swift highlight-backgroundColor-padding try engine.block.setFloat(text, property: "backgroundColor/paddingLeft", value: 8) try engine.block.setFloat(text, property: "backgroundColor/paddingTop", value: 8) try engine.block.setFloat(text, property: "backgroundColor/paddingRight", value: 8) try engine.block.setFloat(text, property: "backgroundColor/paddingBottom", value: 8) ``` Round the corners by setting `backgroundColor/cornerRadius`: ```swift highlight-backgroundColor-cornerRadius try engine.block.setFloat(text, property: "backgroundColor/cornerRadius", value: 12) ``` Text backgrounds inherit the animations assigned to their text block when `textAnimationWritingStyle` is set to `"Block"`: ```swift highlight-backgroundColor-animation let animation = try engine.block.createAnimation(AnimationType.slide) try engine.block.setEnum(animation, property: "textAnimationWritingStyle", value: "Block") try engine.block.setInAnimation(text, animation: animation) try engine.block.setOutAnimation(text, animation: animation) ``` ## Text Case Transformations Render text in a different case without modifying the underlying string. The case is a render-time modifier — `engine.block.replaceText(_:text:)` still returns the original characters. `engine.block.setTextCase(_:textCase:in:)` takes a `TextCase` value: - `.normal` — render the string as stored. - `.uppercase` — render every character in upper case. - `.lowercase` — render every character in lower case. - `.titlecase` — render the first character of each word in upper case. ```swift highlight-setTextCase try engine.block.setTextCase(text, textCase: .titlecase) ``` `engine.block.getTextCases(_:in:)` returns the ordered list of text cases in the requested range: ```swift highlight-getTextCases let textCases = try engine.block.getTextCases(text) print("Text cases: \(textCases)") ``` ## Typefaces and Fonts Use `engine.block.setFont(_:fontFileURL:typeface:)` when you want to change the font and reset existing formatting. Use `engine.block.setTypeface(_:typeface:in:)` when you want to change the typeface while preserving existing formatting. A `Typeface` carries the typeface name and a list of `Font` definitions. Each `Font` needs a `uri` pointing at the font file and a `subFamily` string matching the font's effective name within the typeface. The `weight` and `style` fields let the engine select the right variant when toggling bold or italic. ```swift highlight-setFont let typeface = Typeface( name: "Roboto", fonts: [ Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf"), subFamily: "Bold", weight: .bold, style: .normal, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-BoldItalic.ttf"), subFamily: "Bold Italic", weight: .bold, style: .italic, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Italic.ttf"), subFamily: "Italic", weight: .normal, style: .italic, ), Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), ], ) try engine.block.setFont(text, fontFileURL: typeface.fonts[3].uri, typeface: typeface) ``` `setTypeface(_:typeface:in:)` keeps the current weight and style as much as the new typeface allows. If the new typeface does not support the current combination, the engine falls back to the closest available variant or to the typeface's fallback font: ```swift highlight-setTypeface try engine.block.setTypeface(text, typeface: typeface, in: "CE.SDK".range(of: "E.SDK")!) try engine.block.setTypeface(text, typeface: typeface) ``` Query the current typeface with `engine.block.getTypeface(_:)`. A newly created text block has no explicit typeface set, so this call throws until `setFont(_:fontFileURL:typeface:)` is called for the first time: ```swift highlight-getTypeface let currentDefaultTypeface = try engine.block.getTypeface(text) print("Default typeface: \(currentDefaultTypeface.name)") ``` `engine.block.getTypefaces(_:in:)` returns the ordered list of unique typefaces used across the requested range: ```swift highlight-getTypefaces let currentTypefaces = try engine.block.getTypefaces(text) let currentTypefacesOfRange = try engine.block.getTypefaces(text, in: "CE.SDK".range(of: "E.SDK")!) print("Typefaces across the block: \(currentTypefaces.map(\.name))") print("Typefaces in \"E.SDK\": \(currentTypefacesOfRange.map(\.name))") ``` ## Font Weights and Styles A text block can mix weights and styles across ranges. Toggle between normal and bold, or between normal and italic, using the typed toggle APIs. The active typeface must include a font variant matching the requested combination — otherwise the toggle returns `false` and the call to apply it throws. `engine.block.canToggleBoldFont(_:in:)` reports whether bold can be toggled; `engine.block.toggleBoldFont(_:in:)` applies the toggle: ```swift highlight-toggleBold if try engine.block.canToggleBoldFont(text) { try engine.block.toggleBoldFont(text) } if try engine.block.canToggleBoldFont(text, in: "CE.SDK".range(of: "E.SDK")!) { try engine.block.toggleBoldFont(text, in: "CE.SDK".range(of: "E.SDK")!) } ``` `engine.block.canToggleItalicFont(_:in:)` and `engine.block.toggleItalicFont(_:in:)` work the same way for italic: ```swift highlight-toggleItalic if try engine.block.canToggleItalicFont(text) { try engine.block.toggleItalicFont(text) } if try engine.block.canToggleItalicFont(text, in: "CE.SDK".range(of: "E.SDK")!) { try engine.block.toggleItalicFont(text, in: "CE.SDK".range(of: "E.SDK")!) } ``` To set a font weight directly without toggling, use `engine.block.setTextFontWeight(_:fontWeight:in:)`: ```swift highlight-setTextFontWeight try engine.block.setTextFontWeight(text, fontWeight: .bold) ``` `engine.block.getTextFontWeights(_:in:)` returns the ordered list of unique font weights in the requested range: ```swift highlight-getTextFontWeights let fontWeights = try engine.block.getTextFontWeights(text) print("Font weights: \(fontWeights)") ``` `engine.block.setTextFontStyle(_:fontStyle:in:)` and `engine.block.getTextFontStyles(_:in:)` are the equivalent setter and getter for font styles: ```swift highlight-setTextFontStyle try engine.block.setTextFontStyle(text, fontStyle: .italic) ``` ```swift highlight-getTextFontStyles let fontStyles = try engine.block.getTextFontStyles(text) print("Font styles: \(fontStyles)") ``` ## API Reference ### Methods | Method | Purpose | | --- | --- | | `engine.block.replaceText(_:text:in:)` | Replace or insert text at a Swift string range | | `engine.block.removeText(_:from:)` | Remove text at a Swift string range | | `engine.block.setTextColor(_:color:in:)` | Set the text color for the whole block or a range | | `engine.block.getTextColors(_:in:)` | Get the ordered unique text colors for a range | | `engine.block.setBackgroundColorEnabled(_:enabled:)` | Enable or disable the text background | | `engine.block.setBackgroundColor(_:r:g:b:a:)` | Set the text background color | | `engine.block.getBackgroundColor(_:)` | Read the text background color | | `engine.block.setFloat(_:property:value:)` | Set a numeric block property such as `backgroundColor/paddingLeft` or `backgroundColor/cornerRadius` | | `engine.block.createAnimation(_:)` | Create an animation block | | `engine.block.setEnum(_:property:value:)` | Set an enum-valued property such as `textAnimationWritingStyle` | | `engine.block.setInAnimation(_:animation:)` | Assign an in-animation to the text block | | `engine.block.setOutAnimation(_:animation:)` | Assign an out-animation to the text block | | `engine.block.setTextCase(_:textCase:in:)` | Apply a text case transformation | | `engine.block.getTextCases(_:in:)` | Get the ordered text cases for a range | | `engine.block.setFont(_:fontFileURL:typeface:)` | Change the font and reset existing formatting | | `engine.block.setTypeface(_:typeface:in:)` | Change the typeface while preserving formatting | | `engine.block.getTypeface(_:)` | Get the text block's default typeface | | `engine.block.getTypefaces(_:in:)` | Get the ordered unique typefaces for a range | | `engine.block.canToggleBoldFont(_:in:)` | Check whether bold can be toggled | | `engine.block.toggleBoldFont(_:in:)` | Toggle between normal and bold weight | | `engine.block.canToggleItalicFont(_:in:)` | Check whether italic can be toggled | | `engine.block.toggleItalicFont(_:in:)` | Toggle between normal and italic style | | `engine.block.setTextFontWeight(_:fontWeight:in:)` | Set the font weight for the whole block or a range | | `engine.block.getTextFontWeights(_:in:)` | Get the ordered unique font weights for a range | | `engine.block.setTextFontStyle(_:fontStyle:in:)` | Set the font style for the whole block or a range | | `engine.block.getTextFontStyles(_:in:)` | Get the ordered unique font styles for a range | ### Properties | Property | Type | Description | | --- | --- | --- | | `backgroundColor/paddingLeft` | Float | Padding to the left of the text | | `backgroundColor/paddingRight` | Float | Padding to the right of the text | | `backgroundColor/paddingTop` | Float | Padding above the text | | `backgroundColor/paddingBottom` | Float | Padding below the text | | `backgroundColor/cornerRadius` | Float | Corner radius of the background rectangle | | `textAnimationWritingStyle` | Enum | Set to `"Block"` so the background follows the text block's animations | ## Troubleshooting **`getTypeface(_:)` throws an error** — A new text block has no explicit typeface until `setFont(_:fontFileURL:typeface:)` is called for the first time. **Bold or italic toggle does nothing** — Confirm the active `Typeface` includes a `Font` definition matching the requested `weight` and `style` combination. **Text background is not visible** — Call `setBackgroundColorEnabled(_:enabled:)` with `enabled: true` before changing colors, padding, or corner radius. **Text case looks different from the string value** — Text case transformations affect rendering only. The stored string value is unchanged; reading it back with the engine still returns the original characters. **Formatting resets after changing fonts** — Use `setTypeface(_:typeface:in:)` instead of `setFont(_:fontFileURL:typeface:)` to keep weights, styles, and per-range overrides. --- ## 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 Designs" description: "Create and customize text component libraries using predefined text designs that appear in your asset library." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/text-designs-a1b2c3/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Text Designs](https://img.ly/docs/cesdk/mac-catalyst/text/text-designs-a1b2c3/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-designs/TextDesigns.swift reference-only import Foundation import IMGLYEngine @MainActor func textDesigns(engine: Engine) async throws { // Demo scaffolding: a 1080x1080 sample sheet showing two text designs — a // styled headline at the top and a promotional SALE callout below — to // illustrate the variety of components this workflow produces. The lesson // teaches the workflow with the headline; the SALE block is rendered here // for visual richness in the hero only. Pixel design unit pairs the // font-size unit to pixels so the auto font-size bounds below are // interpreted as pixels. 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) // Decorative SALE callout: a red text on a light-red background graphic. let saleBackground = try engine.block.create(.graphic) try engine.block.setShape(saleBackground, shape: engine.block.createShape(.rect)) try engine.block.setFill(saleBackground, fill: engine.block.createFill(.color)) let saleFill = try engine.block.getFill(saleBackground) try engine.block.setColor(saleFill, property: "fill/color/value", color: .rgba(r: 1.0, g: 0.92, b: 0.86, a: 1.0)) try engine.block.setWidthMode(saleBackground, mode: .absolute) try engine.block.setHeightMode(saleBackground, mode: .absolute) try engine.block.setWidth(saleBackground, value: 720) try engine.block.setHeight(saleBackground, value: 280) try engine.block.setPositionX(saleBackground, value: 180) try engine.block.setPositionY(saleBackground, value: 680) try engine.block.appendChild(to: page, child: saleBackground) let saleText = try engine.block.create(.text) try engine.block.replaceText(saleText, text: "SALE 50%") try engine.block.setTextFontSize(saleText, fontSize: 140) try engine.block.setTextColor(saleText, color: .rgba(r: 0.78, g: 0.16, b: 0.16, a: 1.0)) try engine.block.setWidthMode(saleText, mode: .absolute) try engine.block.setHeightMode(saleText, mode: .absolute) try engine.block.setWidth(saleText, value: 720) try engine.block.setHeight(saleText, value: 280) try engine.block.setPositionX(saleText, value: 180) try engine.block.setPositionY(saleText, value: 680) try engine.block.appendChild(to: page, child: saleText) // Subtitle copy explaining what readers see — visual filler for the hero. let subtitle = try engine.block.create(.text) try engine.block.replaceText(subtitle, text: "Reusable text designs you can save and apply") try engine.block.setTextFontSize(subtitle, fontSize: 44) try engine.block.setTextColor(subtitle, color: .rgba(r: 0.40, g: 0.45, b: 0.52, a: 1.0)) try engine.block.setWidthMode(subtitle, mode: .absolute) try engine.block.setHeightMode(subtitle, mode: .absolute) try engine.block.setWidth(subtitle, value: 800) try engine.block.setHeight(subtitle, value: 120) try engine.block.setPositionX(subtitle, value: 140) try engine.block.setPositionY(subtitle, value: 470) try engine.block.appendChild(to: page, child: subtitle) let component = try engine.block.create(.text) try engine.block.replaceText(component, text: "Headline") try engine.block.setTextFontSize(component, fontSize: 160) try engine.block.setTextColor(component, color: .rgba(r: 0.122, g: 0.161, b: 0.216, a: 1.0)) try engine.block.setWidthMode(component, mode: .absolute) try engine.block.setHeightMode(component, mode: .absolute) try engine.block.setWidth(component, value: 800) try engine.block.setHeight(component, value: 280) try engine.block.setPositionX(component, value: 140) try engine.block.setPositionY(component, value: 160) try engine.block.appendChild(to: page, child: component) try engine.block.setBool(component, property: "text/clipLinesOutsideOfFrame", value: true) try engine.block.setBool(component, property: "text/automaticFontSizeEnabled", value: true) try engine.block.setFloat(component, property: "text/minAutomaticFontSize", value: 32) try engine.block.setFloat(component, property: "text/maxAutomaticFontSize", value: 200) try await engine.captureGuide(page, label: "hero") let archive = try await engine.block.saveToArchive(blocks: [component]) let archiveURL = FileManager.default.temporaryDirectory .appendingPathComponent("text-design-\(UUID().uuidString).zip") try archive.write(to: archiveURL) let thumbnail = try await engine.block.export( component, mimeType: .png, options: ExportOptions(targetWidth: 400, targetHeight: 320), ) let thumbnailURL = FileManager.default.temporaryDirectory .appendingPathComponent("text-design-\(UUID().uuidString).png") try thumbnail.write(to: thumbnailURL) try engine.asset.addLocalSource(sourceID: "my-text-components", applyAsset: { [weak engine] asset in guard let engine, let uri = asset.meta?["uri"], let url = URL(string: uri) else { return nil } let loaded = try await engine.block.loadArchive(from: url) guard let newBlock = loaded.first else { return nil } if let currentPage = try await engine.scene.getCurrentPage() { try await engine.block.appendChild(to: currentPage, child: newBlock) } return newBlock }) let component1 = AssetDefinition( id: "ly.img.text.components.headline", meta: [ "uri": archiveURL.absoluteString, "thumbUri": thumbnailURL.absoluteString, "mimeType": "application/ubq-blocks-archive", ], label: ["en": "Headline", "de": "Überschrift"], ) try engine.asset.addAsset(to: "my-text-components", asset: component1) try engine.asset.assetSourceContentsChanged(sourceID: "my-text-components") let contentJSONURL = URL( string: "https://your-backend.example.com/assets/ly.img.text.components/content.json", )! // try await engine.asset.addLocalAssetSourceFromJSON(contentJSONURL) _ = contentJSONURL } ``` Build a library of reusable text components — pre-designed, pre-styled text layouts that appear in your asset library and drop into the user's scene with a tap. ![A sample sheet of custom text designs — a dark navy 'Headline' at the top, a gray subtitle, and a red 'SALE 50%' callout on a light red background — illustrating the variety of components saved and registered through this workflow.](./assets/swift-based.hero.webp) > **Reading time:** 8 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-text-designs) Text designs (also known as text components) are serialized text blocks with styling, constraints, and behavior baked in. Users browse them in the asset library and tap to insert one — the engine loads the bundled archive, attaches the resulting block to the current page, and the user is editing in place. ## What are Text Designs? A text design is a `.zip` archive produced by `engine.block.saveToArchive(blocks:)`. The archive packs the block hierarchy together with every resource it references (fonts, images), so the component is self-contained and portable across environments. Each design pairs with a PNG thumbnail rendered to fit the asset library's cell. ## Default Components CE.SDK ships with over 20 pre-built text designs covering common layouts — bold headlines, quotes, callouts, romantic and handwritten styles, promotional banners, and more. They live in the `ly.img.text.components` asset source and are available to every editor out of the box. This guide shows how to add your own components alongside (or in place of) the defaults. ## Content.json Structure Text designs are served from a `content.json` file using version `"5.0.0"` of the asset source schema. Each entry references either the `.blocks` file from an extracted archive (matching the layout of CE.SDK's built-in `ly.img.text.components`) or the archive itself, and a thumbnail. ```json { "version": "5.0.0", "id": "ly.img.text.components", "assets": [ { "id": "ly.img.text.components.headline", "label": { "en": "Headline", "de": "Überschrift" }, "meta": { "uri": "{{base_url}}/ly.img.text.components/data/headline/blocks.blocks", "thumbUri": "{{base_url}}/ly.img.text.components/thumbnails/headline.png", "mimeType": "application/ubq-blocks-string" } } ], "blocks": [] } ``` Each asset entry requires: - `id` — Unique identifier, conventionally `ly.img.text.components.` - `label` — Localized display name. `IMGLYEngine.Locale` is a `String` typealias, so plain language keys work (`["en": "Headline"]`) - `meta.uri` — URL to the asset payload. Use `application/ubq-blocks-string` when the URL points to a `blocks.blocks` file inside an extracted archive, and `application/ubq-blocks-archive` when the URL points to the `.zip` archive directly. - `meta.thumbUri` — URL to the PNG preview - `meta.mimeType` — `"application/ubq-blocks-string"` (extracted archive layout, matching the built-in source) or `"application/ubq-blocks-archive"` (raw `.zip`) `{{base_url}}` resolves to the directory that contains the `content.json` you register with `addLocalAssetSourceFromJSON(_:matcher:)`. ## Creating Custom Components ### Design Your Component Create a text block and style it. Use absolute width and height so the component holds its frame when inserted, and place it at known coordinates so the export below crops predictably. ```swift highlight-textDesigns-designComponent let component = try engine.block.create(.text) try engine.block.replaceText(component, text: "Headline") try engine.block.setTextFontSize(component, fontSize: 160) try engine.block.setTextColor(component, color: .rgba(r: 0.122, g: 0.161, b: 0.216, a: 1.0)) try engine.block.setWidthMode(component, mode: .absolute) try engine.block.setHeightMode(component, mode: .absolute) try engine.block.setWidth(component, value: 800) try engine.block.setHeight(component, value: 280) try engine.block.setPositionX(component, value: 140) try engine.block.setPositionY(component, value: 160) try engine.block.appendChild(to: page, child: component) ``` ### Configure Constraints The two settings that make a text design behave like a component — rather than a free-form text block — are clipping and automatic font-size. Clipping is already on by default for new text blocks (`text/clipLinesOutsideOfFrame` defaults to `true`), so the explicit `setBool` documents the intent and locks the value. Automatic font-size, in contrast, defaults to `false`; enabling it with a min/max range lets the text scale to fit between the bounds you choose, so the layout looks balanced for any reasonable amount of text. ```swift highlight-textDesigns-constraints try engine.block.setBool(component, property: "text/clipLinesOutsideOfFrame", value: true) try engine.block.setBool(component, property: "text/automaticFontSizeEnabled", value: true) try engine.block.setFloat(component, property: "text/minAutomaticFontSize", value: 32) try engine.block.setFloat(component, property: "text/maxAutomaticFontSize", value: 200) ``` ### Serialize to an Archive `engine.block.saveToArchive(blocks:)` returns a `Blob` (a `Data` typealias) holding a `.zip` archive with `blocks.blocks` plus every referenced resource. Write the archive to a known location — temporary storage in the example, your hosting bucket in production. ```swift highlight-textDesigns-saveArchive let archive = try await engine.block.saveToArchive(blocks: [component]) let archiveURL = FileManager.default.temporaryDirectory .appendingPathComponent("text-design-\(UUID().uuidString).zip") try archive.write(to: archiveURL) ``` Bundling resources into the archive is the recommended approach because the component stays self-contained: the engine resolves fonts and images from the archive directly, so the component works in any environment that can read it. `engine.block.saveToString(blocks:)` is the legacy alternative, but it leaves resources as external URLs that must remain reachable at load time and requires you to configure `allowedResourceSchemes`. Prefer `saveToArchive`. ### Generate a Thumbnail Render a PNG preview with `engine.block.export(_:mimeType:options:)` so the asset library grid has something to display. `ExportOptions(targetWidth:targetHeight:)` controls the minimum render size; the engine scales the block so it fills the target in at least one axis while preserving the block's aspect ratio. With an 800×240 block and a 400×320 target, the resulting image is 1067×320 — wider than 400, exactly 320 tall. ```swift highlight-textDesigns-thumbnail let thumbnail = try await engine.block.export( component, mimeType: .png, options: ExportOptions(targetWidth: 400, targetHeight: 320), ) let thumbnailURL = FileManager.default.temporaryDirectory .appendingPathComponent("text-design-\(UUID().uuidString).png") try thumbnail.write(to: thumbnailURL) ``` ## Registering the Asset Source Register a local asset source with `engine.asset.addLocalSource(sourceID:applyAsset:)`. The `applyAsset` closure runs each time a user picks a component: it reads the asset's `meta["uri"]`, calls `engine.block.loadArchive(from:)`, and appends the resulting block to the current page. Capture `engine` weakly — the source retains the callback for its lifetime, so a strong capture would risk a retain cycle. ```swift highlight-textDesigns-registerSource try engine.asset.addLocalSource(sourceID: "my-text-components", applyAsset: { [weak engine] asset in guard let engine, let uri = asset.meta?["uri"], let url = URL(string: uri) else { return nil } let loaded = try await engine.block.loadArchive(from: url) guard let newBlock = loaded.first else { return nil } if let currentPage = try await engine.scene.getCurrentPage() { try await engine.block.appendChild(to: currentPage, child: newBlock) } return newBlock }) ``` Then register each component with `engine.asset.addAsset(to:asset:)`, passing an `AssetDefinition` that carries the display label and URLs. The runtime example below points `meta["uri"]` at the `.zip` archive on disk and tags it with `application/ubq-blocks-archive`. Call `engine.asset.assetSourceContentsChanged(sourceID:)` after batched changes so the library refreshes. ```swift highlight-textDesigns-addAsset let component1 = AssetDefinition( id: "ly.img.text.components.headline", meta: [ "uri": archiveURL.absoluteString, "thumbUri": thumbnailURL.absoluteString, "mimeType": "application/ubq-blocks-archive", ], label: ["en": "Headline", "de": "Überschrift"], ) try engine.asset.addAsset(to: "my-text-components", asset: component1) try engine.asset.assetSourceContentsChanged(sourceID: "my-text-components") ``` ## Hosting Custom Components For production, extract the archive on your server so it lives as a directory of static files alongside the thumbnail and a `content.json`: ``` /ly.img.text.components/ ├── content.json ├── data/ │ ├── headline/ │ │ ├── blocks.blocks │ │ ├── fonts/... │ │ └── images/... │ └── ... └── thumbnails/ ├── headline.png └── ... ``` Register the source from the hosted `content.json` URL with `engine.asset.addLocalAssetSourceFromJSON(_:matcher:)`. The engine resolves every `{{base_url}}` placeholder against the parent directory of the URL you pass. ```swift highlight-textDesigns-loadFromJson let contentJSONURL = URL( string: "https://your-backend.example.com/assets/ly.img.text.components/content.json", )! // try await engine.asset.addLocalAssetSourceFromJSON(contentJSONURL) ``` See [Serve Assets](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) for guidance on routing CE.SDK to a custom backend. ## Troubleshooting | Issue | Cause | Solution | | --- | --- | --- | | Component fails to load | `meta["uri"]` points to a missing or unreadable archive | Verify the URL is reachable and the archive has not been truncated | | Component appears unstyled | Fonts are referenced by external URL and that URL is unreachable | Use `saveToArchive` so fonts are bundled into the archive instead of left as external references | | Inserted text scales unexpectedly | Width or height is in `.auto` mode | Set both modes to `.absolute` with `setWidthMode(_:mode:)` and `setHeightMode(_:mode:)` so the frame stays fixed | | User input overflows the frame | `text/clipLinesOutsideOfFrame` is `false` | Set the property to `true` so long input is clipped rather than expanding the frame | | Thumbnail is the wrong size | `ExportOptions.targetWidth` and `targetHeight` were not set, or the block's aspect ratio differs from the target | Pass both values to `ExportOptions`. The engine fills the target in at least one axis and preserves the block's aspect ratio, so the output is at least the target size; design the source block at the same aspect ratio if you need an exact match. | ## API Reference | Method | Description | | --- | --- | | `engine.block.saveToArchive(blocks:)` | Save blocks to a `.zip` archive bundling every resource | | `engine.block.loadArchive(from:)` | Load blocks from an archive URL | | `engine.block.export(_:mimeType:options:)` | Export a block as image data with target dimensions | | `engine.block.setBool(_:property:value:)` | Set a boolean property (`text/clipLinesOutsideOfFrame`, `text/automaticFontSizeEnabled`) | | `engine.block.setFloat(_:property:value:)` | Set a float property (`text/minAutomaticFontSize`, `text/maxAutomaticFontSize`) | | `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.assetSourceContentsChanged(sourceID:)` | Notify the UI that a source's contents changed | | `engine.asset.addLocalAssetSourceFromJSON(_:matcher:)` | Register an asset source from a hosted `content.json` URL | ## Next Steps - [Serve Assets](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) — Configure CE.SDK to load assets from your own backend - [Text Overview](https://img.ly/docs/cesdk/mac-catalyst/text/overview-0bd620/) — Add, style, and customize text layers in your design using CE.SDK's flexible text editing tools - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) — Apply colors, backgrounds, and typeface styling programmatically - [Auto-Size](https://img.ly/docs/cesdk/mac-catalyst/text/auto-size-5331b3/) — Configure text blocks to auto-size based on content --- ## 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 on a Path" description: "Place text along an SVG path — a circle, arch, wave, or any curve — with the setTextOnPath engine API or, on iOS, the editor's built-in Path inspector." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/text/text-on-path-e3b8a2/" --- > 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 Text](https://img.ly/docs/cesdk/mac-catalyst/text-8a993a/) > [Text on a Path](https://img.ly/docs/cesdk/mac-catalyst/text/text-on-path-e3b8a2/) --- ```swift file=@cesdk_swift_examples/engine-guides-text-on-path/TextOnPath.swift reference-only import IMGLYEngine @MainActor func textOnPath(engine: Engine) async throws { // Demo scaffolding: a square page that frames the curved text for the // captures below. Creating the scene with `designUnit: .px` pairs the // font-size unit to Pixel, so the `setTextFontSize` value below renders at // the scale the SVG path's local coordinates assume. `setTextOnPath` sizes // the block to span about 7 font heights of the curve's larger dimension, // so the page needs to be generous relative to the font size below or the // curve renders larger than the page (and the capture goes blank). let scene = try engine.scene.create(designUnit: .px) let page = try engine.block.create(.page) try engine.block.setWidth(page, value: 480) try engine.block.setHeight(page, value: 480) try engine.block.appendChild(to: scene, child: page) let text = try engine.block.create(.text) try engine.block.appendChild(to: page, child: text) try engine.block.replaceText(text, text: "TEXT ON A PATH") try engine.block.setTextFontSize(text, fontSize: 48) let circlePath = "M 60,119.5 A 59.5,59.5 0 1,1 60.01,119.5 Z" try engine.block.setTextOnPath(text, svgPath: circlePath) // Demo scaffolding: setTextOnPath resizes the block to the path's aspect // ratio, so center it on the page now that its size is known. let width = try engine.block.getWidth(text) let height = try engine.block.getHeight(text) try engine.block.setPositionX(text, value: (480 - width) / 2) try engine.block.setPositionY(text, value: (480 - height) / 2) try await engine.captureGuide(page, label: "after-place-on-path") try engine.block.setEnum(text, property: "text/verticalAlignment", value: "Center") try await engine.captureGuide(page, label: "after-vertical-position") try engine.block.setTextOnPathOffset(text, offset: 0.05) let pathOffset = try engine.block.getTextOnPathOffset(text) print("Path offset:", pathOffset) try await engine.captureGuide(page, label: "hero") try engine.block.setTextOnPathFlipped(text, flipped: true) let isFlipped = try engine.block.getTextOnPathFlipped(text) print("Flipped:", isFlipped) try await engine.captureGuide(page, label: "after-flip") let currentPath = try engine.block.getTextOnPath(text) print("Text on path:", currentPath ?? "none") try engine.block.setTextOnPath(text, svgPath: nil) try await engine.captureGuide(page, label: "after-clear") } ``` Curve a text block so its characters follow an SVG path — an arch, a full circle, or any custom curve — instead of a straight baseline. ![The words TEXT ON A PATH curving along the left arc of a circular path, with vertical alignment Center and a slight offset along the path](./assets/swift-based.hero.webp) > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-text-on-path) Text on a path makes a text block's baseline follow an SVG curve instead of a straight line — useful for badges, circular seals, and arched headlines. It applies to text blocks only and disables word wrapping while a path is active: explicit line breaks in the text collapse to spaces. Setting a path resizes the block to match the path's aspect ratio. ## Using the Built-in Path UI On iOS, CE.SDK's prebuilt editor lets users curve text interactively: select a text block and a **Path** button appears in the inspector bar. Tapping it opens a sheet of curve tiles, and **None** removes the path and restores the straight baseline. The tiles — **Circle**, **Arch**, **Wave**, and **Elevate** by default — are defined by the `ly.img.text.curves` asset source rather than the app, so the set can change with future asset versions or your own presets. Each tile is a style preset, so tapping one does more than bend the baseline: it also replaces the block's text content and horizontal alignment with the preset's own values — expect a typed headline to be overwritten when a tile is applied. While a path is active — whether applied from a tile or set programmatically with a custom SVG string — the sheet shows **Path Position** (Top, Center, Bottom), **Direction** (Forward, Reversed), and an **Offset** slider below the tiles. The rest of this guide covers the Engine API behind that experience, which applies text on a path programmatically on every Apple platform. ## Applying Curved Text Presets from the Asset Library The same **Curved Text** presets shown in the Path sheet also surface in the asset library's text section, and you can query the source directly with `engine.asset.findAssets(sourceID:query:)`. Applying one from either place is the same operation: it sets the block's path, offset, and vertical alignment together in one step and, because each tile is a style preset, replaces the block's text content and horizontal alignment with the preset's own values. ## Creating the Text Block We create a text block, give it a short headline, and set a font size. Creating and styling text in depth is covered in [Add Text](https://img.ly/docs/cesdk/mac-catalyst/text/add-4f5011/) and [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/). ```swift highlight-textOnPath-createText let text = try engine.block.create(.text) try engine.block.appendChild(to: page, child: text) try engine.block.replaceText(text, text: "TEXT ON A PATH") try engine.block.setTextFontSize(text, fontSize: 48) ``` The block starts out with a normal, straight baseline. ## Placing Text on the Path We curve the text onto a circle with `setTextOnPath(_:svgPath:)`, passing an SVG path string in the block's local coordinate space. The path must contain exactly one subpath (a single leading `M`); the block resizes to match the path's aspect ratio and word wrapping turns off. ```swift highlight-textOnPath-placeOnPath let circlePath = "M 60,119.5 A 59.5,59.5 0 1,1 60.01,119.5 Z" try engine.block.setTextOnPath(text, svgPath: circlePath) ``` The characters now follow the circle instead of a straight line. ## Positioning the Text Vertically We set where the text sits relative to the path with `setEnum(_:property:value:)` on the `text/verticalAlignment` property — `Top`, `Center`, or `Bottom`. ```swift highlight-textOnPath-verticalPosition try engine.block.setEnum(text, property: "text/verticalAlignment", value: "Center") ``` `Center` runs the baseline through the middle of the glyphs, while `Top` and `Bottom` sit the text on the inner or outer edge of the curve. ## Offsetting Along the Path We slide the text along the path with `setTextOnPathOffset(_:offset:)`, a proportional value in the range `[-1, 1]` centered at zero, and read it back with `getTextOnPathOffset(_:)`. The same value is exposed as the generic `text/pathOffset` float property. Both ends of the range wrap back to the same position as `0`, so `1` and `-1` don't move the text further than a value just short of them. ```swift highlight-textOnPath-offset try engine.block.setTextOnPathOffset(text, offset: 0.05) let pathOffset = try engine.block.getTextOnPathOffset(text) print("Path offset:", pathOffset) ``` Positive values move the text forward along the path; negative values move it back. ## Flipping the Direction We flip the text to the other side of the curve — reversing its direction — with `setTextOnPathFlipped(_:flipped:)`, and read whether it's flipped back with `getTextOnPathFlipped(_:)`. ```swift highlight-textOnPath-direction try engine.block.setTextOnPathFlipped(text, flipped: true) let isFlipped = try engine.block.getTextOnPathFlipped(text) print("Flipped:", isFlipped) ``` Flipping is what turns the bottom half of a circular badge right-side up. ## Reading and Clearing the Path We read the block's current path with `getTextOnPath(_:)`, which returns the SVG string or `nil`. To remove the curve and restore a straight, auto-sized baseline, pass `nil` to `setTextOnPath(_:svgPath:)`. ```swift highlight-textOnPath-readAndClear let currentPath = try engine.block.getTextOnPath(text) print("Text on path:", currentPath ?? "none") try engine.block.setTextOnPath(text, svgPath: nil) ``` Clearing the path returns the block to a normal, straight text block with automatic sizing. ## Configuring Availability On iOS, the Path sheet is gated as a whole through its inspector-bar button — pass `isVisible: { _ in false }` to `InspectorBar.Buttons.textOnPath(action:title:icon:isEnabled:isVisible:)` to remove it, or `isEnabled: { _ in false }` to keep it visible but inactive. There's no separate control for hiding only the offset slider or only the curve tiles, and the button's closures don't affect the other surfaces that list the same `ly.img.text.curves` presets — the text-presets sheet and the asset library's text section. See [Disable or Enable Features](#broken-link-f058e2) for the general `isVisible`/`isEnabled` pattern used across dock, inspector bar, and canvas menu buttons. ## Troubleshooting - **The path string is rejected**: `setTextOnPath(_:svgPath:)` throws an `EngineError` whose `catalogCode` is `EngineErrorCode.blockTextOnPathInvalidSvgPath` when the value isn't a valid SVG path — check the `d` attribute. - **The path has multiple subpaths**: throws with `catalogCode` `EngineErrorCode.blockTextOnPathMultipleSubpaths` — supply a single continuous contour with one leading `M`. - **The path has no measurable length**: throws with `catalogCode` `EngineErrorCode.blockTextOnPathNoMeasurableContour` — a lone `M` has zero length; give the path drawable length. - **Text runs off the end of the path**: the block isn't grown to fit the text, so text longer than the path overflows. Shorten the text or enlarge the block. - **The Path button doesn't appear**: on iOS, the selection isn't a text block, or its `text/character` scope isn't allowed (all scopes are allowed under the default Creator role). ## API Reference ### Methods | Method | Description | | --- | --- | | `engine.block.create(_:)` | Create the text block to curve | | `engine.block.replaceText(_:text:in:)` | Set the block's text content | | `engine.block.setTextFontSize(_:fontSize:in:)` | Set the block's font size | | `engine.block.setTextOnPath(_:svgPath:)` | Curve a text block along an SVG path (single subpath); pass `nil` to clear | | `engine.block.getTextOnPath(_:)` | Get the block's current path SVG string, or `nil` | | `engine.block.setTextOnPathOffset(_:offset:)` | Set the proportional offset (`[-1, 1]`) of the text along the path | | `engine.block.getTextOnPathOffset(_:)` | Get the current path offset | | `engine.block.setTextOnPathFlipped(_:flipped:)` | Flip the text to the other side of the path (reverse direction) | | `engine.block.getTextOnPathFlipped(_:)` | Get whether the text is flipped | | `engine.block.setEnum(_:property:value:)` | Set `text/verticalAlignment` (`Top`/`Center`/`Bottom`) relative to the path | ### Properties | Property | Type | Description | | --- | --- | --- | | `text/verticalAlignment` | String enum | Position relative to the path — `Top`, `Center`, or `Bottom` (default `Top`); set with `setEnum(_:property:value:)` | | `text/pathOffset` | Float | Equivalent to `setTextOnPathOffset(_:offset:)` — the proportional offset (`[-1, 1]`) along the path | ## Next Steps - [Text Styling](https://img.ly/docs/cesdk/mac-catalyst/text/styling-269c48/) — fonts, sizing, color, and alignment for the curved text - [Text Effects](https://img.ly/docs/cesdk/mac-catalyst/text/effects-2dc9fc/) — add shadows and effects to the text - [Disable or Enable Features](#broken-link-f058e2) — on iOS, gate the Path button and other editor components with the `isVisible`/`isEnabled` pattern --- ## 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 v1.19" description: "Learn what changed in v1.19 and how to update your implementation to stay compatible." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/to-v1-19-55bcad/" --- > 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:** [Upgrading](https://img.ly/docs/cesdk/mac-catalyst/upgrade-4f8715/) > [To v1.19](https://img.ly/docs/cesdk/mac-catalyst/to-v1-19-55bcad/) --- Version v1.19 of CreativeEngineSDK and CreativeEditorSDK introduces structural changes to many of the current design blocks, making them more composable and more powerful. Along with this update, there are mandatory license changes that require attention. This comes with a number of breaking changes. This document will explain the changes and describe the steps you need to take to adapt them to your setup. ## **Initialization** The initialization of the `Engine` has changed. Now the `Engine` initializer is async and failable. It also requires a new parameter `license` which is the API key you received from our dashboard. There is also a new optional parameter `userID` an optional unique ID tied to your application's user. This helps us accurately calculate monthly active users (MAU). Especially useful when one person uses the app on multiple devices with a sign-in feature, ensuring they're counted once. Providing this aids in better data accuracy. ```swift try await Engine(license: "", userID: "") ``` Please see the [updated Quickstarts](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) for complete SwiftUI, UIKit, and AppKit integration examples. ## **DesignBlockType** These are the transformations of all `DesignBlockType` types: Removed: - `DesignBlockType.image` - `DesignBlockType.video` - `DesignBlockType.sticker` - `DesignBlockType.vectorPath` - `DesignBlockType.rectShape` - `DesignBlockType.lineShape` - `DesignBlockType.starShape` - `DesignBlockType.polygonShape` - `DesignBlockType.ellipseShape` - `DesignBlockType.colorFill` - `DesignBlockType.imageFill` - `DesignBlockType.videoFill` - `DesignBlockType.linearGradientFill` - `DesignBlockType.radialGradientFill` - `DesignBlockType.conicalGradientFill` Added: - `DesignBlockType.graphic` - `DesignBlockType.cutout` Note that `DesignBlockType.allCases` can be used to get the list of all instances mentioned above. ## **Graphic Design Block** A new generic `DesignBlockType.graphic` type has been introduced, that forms the basis of the new unified block structure. ## **Shapes** Similar to how the fill of a block is a separate object which can be attached to and replaced on a design block, we have now introduced a similar concept for the shape of a block. You use the new `createShape`, `getShape` and `setShape` APIs in order to define the shape of a design block. Only the new `DesignBlockType.graphic` block allows to change its shape with these APIs. The new available shape types are: - `ShapeType.rect` - `ShapeType.line` - `ShapeType.ellipse` - `ShapeType.polygon` - `ShapeType.star` - `ShapeType.vectorPath` Note that `ShapeType.allCases` can be used to get the list of all instances mentioned above. The following design block types are now removed in favor of using a `DesignBlockType.graphic` block with one of the above mentioned shape instances: - `DesignBlockType.rectShape` - `DesignBlockType.lineShape` - `DesignBlockType.ellipseShape` - `DesignBlockType.polygonShape` - `DesignBlockType.starShape` - `DesignBlockType.vectorPath` This structural change means that the shape-specific properties (e.g. the number of sides of a polygon) are not available on the design block anymore but on the shape instances instead. You will have to add calls to `getShape` to get the instance id of the shape instance and then pass that to the property getter and setter APIs. Also, remember to change property key strings in the getter and setter calls from plural `shapes/…` to singular `shape/…` to match the new type identifiers. ## **Image and Sticker** Previously, `DesignBlockType.image` and `DesignBlockType.sticker` were their own high-level design block types. They neither support the fill APIs nor the effects APIs. Both of these blocks are now removed in favor of using a `DesignBlockType.graphic` block with an image fill (`FillType.image`) and using the effects APIs instead of the legacy image block’s numerous effects properties. At its core, the sticker block has always just been an image block that is heavily limited in its capabilities. You can neither crop it, nor apply any effects to it. In order to replicate the difference as closely as possible in the new unified structure, more fine-grained scopes have been added. You can now limit the adopter’s ability to crop a block and to edit its appearance. Note that since these scopes only apply to a user of the editor with the “Adopter” role, a “Creator” user will now have all of the same editing options for both images and for blocks that used to be stickers. ## **Scopes** The following is the list of changes to the design block scopes: - (Breaking) The permission to crop a block was split from `content/replace` and `design/style` into a separate scope: `layer/crop`. - Deprecated the `design/arrange` scope and renamed `design/arrange/move` → `layer/move` `design/arrange/resize` → `layer/resize` `design/arrange/rotate` → `layer/rotate` `design/arrange/flip` → `layer/flip` - Deprecated the `content/replace` scope. For `DesignBlockType.Text` blocks, it is replaced with the new `text/edit` scope. For other blocks it is replaced with `fill/change`. - Deprecated the `design/style` scope and replaced it with the following fine-grained scopes: `text/character`, `stroke/change`, `layer/opacity`, `layer/blendMode`, `layer/visibility`, `layer/clipping`, `appearance/adjustments`, `appearance/filter`, `appearance/effect`, `appearance/blur`, `appearance/shadow` - Introduced `fill/change`, `stroke/change`, and `shape/change` scopes that control whether the fill, stroke or shape of a block may be edited by a user with an "Adopter" role. - The deprecated scopes are automatically mapped to their new corresponding scopes by the scope APIs for now until they will be removed completely in a future update. ## **Kind** While the new unified block structure both simplifies a lot of code and makes design blocks more powerful, it also means that many of the design blocks that used to have unique type ids now all have the same generic `DesignBlockType.graphic` type, which means that calls to the `findByType` cannot be used to filter blocks based on their legacy type ids any more. Simultaneously, there are many instances in which different blocks in the scene which might have the same type and underlying technical structure have different semantic roles in the document and should therefore be treated differently by the user interface. To solve both of these problems, we have introduced the concept of a block “kind”. This is a mutable string that can be used to tag different blocks with a semantic label. You can get the kind of a block using the `getKind` API and you can query blocks with a specific kind using the `findByKind` API. CreativeEngine provides the following default kind values: - image - video - sticker - scene - camera - stack - page - audio - text - shape - group Unlike the immutable design block type id, you can change the kind of a block with the new `setKind` API. It is important to remember that the underlying structure and properties of a design block are not strictly defined by its kind, since the kind, shape, fill and effects of a block can be changed independent of each other. Therefore, a user-interface should not make assumptions about available properties of a block purely based on its kind. > **Note:** **Note**Due to legacy reasons, blocks with the kind "sticker" will continue > to not allow their contents to be cropped. This special behavior will be > addressed and replaced with a more general-purpose implementation in a future > update. ​ ## **Asset Definitions** The asset definitions have been updated to reflect the deprecation of legacy block type ids and the introduction of the “kind” property. In addition to the “blockType” meta property, you can now also define the `“shapeType”` ,`“fillType”` and `“kind”` of the block that should be created by the default implementation of the applyAsset function. - `“blockType”` defaults to `DesignBlockType.graphic.rawValue (“//ly.img.ubq/graphic”)` if left unspecified. - `“shapeType”` defaults to `ShapeType.rect.rawValue (“//ly.img.ubq/shape/rect”)` if left unspecified - `“fillType”` defaults to `FillType.color.rawValue (“//ly.img.ubq/fill/color”)` if left unspecified Video block asset definitions used to specify the `“blockType”` as `“//ly.img.ubq/fill/video“ (FillType.video.rawValue)`. The `“fillType”` meta asset property should now be used instead for such fill type ids. ## **Automatic Migration** CreativeEngine will always continue to support scene files that contain the now removed legacy block types. Those design blocks will be automatically replaced by the equivalent new unified block structure when the scene is loaded, which means that the types of all legacy blocks will change to `DesignBlockType.graphic`. Note that this can mean that a block gains new capabilities that it did not have before. For example, the line shape block did not have any stroke properties, so the `hasStroke` API used to return `false`. However, after the automatic migration its `DesignBlockType.graphic` design block replacement supports both strokes and fills, so the `hasStroke` API now returns `true` . Similarly, the image block did not support fills or effects, but the `DesignBlockType.graphic` block does. ## **Types and API Signatures** To improve the type safety of our APIs, we have moved away from using a single `DesignBlockType` enum and split it into multiple types (revised `DesignBlockType`, `FillType`, `EffectType`, and `BlurType`). Those changes have affected the following APIs: - `BlockAPI.create(_:)` - `BlockAPI.createFill(_:)` - `BlockAPI.createEffect(_:)` - `BlockAPI.createBlur(_:)` - `BlockAPI.find(byType:)` > **Note:** **Note**All the functions above still support the string overload variants, however, their > usage will cause lint warnings in favor of type safe overloads. > **Note:** **Attention**`find(byType:)` now provides overloads for `DesignBlockType` and the new `FillType`. > If the type-inferred `find(byType: .image)` version is used it would still compile > without warnings but it now returns image fills (`FillType.image`) and not the > removed legacy high-level image design block types (`DesignBlockType.image`) anymore. > Please see the below "Block Exploration" example to "Query all images in the scene > after migration" to migrate your code base. ## **Code Examples** This section will show some code examples of the breaking changes and how it would look like after migrating. ```swift /** Block Creation */ // Creating an Image before migration let image = try engine.block.create(.image) try engine.block.setString( image, property: "image/imageFileURI", value: "https://domain.com/link-to-image.jpg" ) // Creating an Image after migration let block = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) let imageFill = try engine.block.createFill(.image) try engine.block.setString( imageFill, property: "fill/image/imageFileURI", value: "https://domain.com/link-to-image.jpg" ) try engine.block.setShape(block, shape: rectShape) try engine.block.setFill(block, fill: imageFill) try engine.block.setKind(block, kind: "image") // Creating a star shape before migration let star = try engine.block.create(.starShape) try engine.block.setInt(star, property: "shapes/star/points", value: 8) // Creating a star shape after migration let block = try engine.block.create(.graphic) let starShape = try engine.block.createShape(.star) let colorFill = try engine.block.createFill(.color) try engine.block.setInt(starShape, property: "shape/star/points", value: 8) try engine.block.setShape(block, shape: starShape) try engine.block.setFill(block, fill: colorFill) try engine.block.setKind(block, kind: "shape") // Creating a sticker before migration let sticker = try engine.block.create(.sticker) try engine.block.setString( sticker, property: "sticker/imageFileURI", value: "https://domain.com/link-to-sticker.png" ) // Creating a sticker after migration let block = try engine.block.create(.graphic) let rectShape = try engine.block.createShape(.rect) let imageFill = try engine.block.createFill(.image) try engine.block.setString( imageFill, property: "fill/image/imageFileURI", value: "https://domain.com/link-to-sticker.png" ) try engine.block.setShape(block, shape: rectShape) try engine.block.setFill(block, fill: imageFill) try engine.block.setKind(block, kind: "sticker") /** Block Creation */ ``` ```swift /** Block Exploration */ // Query all images in the scene before migration let images = try engine.block.find(byType: .image) // Query all images in the scene after migration let images = try engine.block.find(byType: .graphic).filter { block in let fill = try engine.block.getFill(block) return try engine.block.isValid(fill) && engine.block.getType(fill) == FillType.image.rawValue } // Query all stickers in the scene before migration let stickers = try engine.block.find(byType: .sticker) // Query all stickers in the scene after migration let stickers = try engine.block.find(byKind: "sticker") // Query all Polygon shapes in the scene before migration let polygons = engine.block.find(byType: .polygonShape) // Query all Polygon shapes in the scene after migration let polygons = try engine.block.find(byType: .graphic).filter { block in let shape = try engine.block.getShape(block) return try engine.block.isValid(shape) && engine.block.getType(shape) == ShapeType.polygon.rawValue } /** Block Exploration */ ``` --- ## 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: "Upgrade" description: "Learn how to upgrade CE.SDK and apply required changes when migrating between major SDK versions." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/upgrade-4f8715/" --- > 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:** [Upgrading](https://img.ly/docs/cesdk/mac-catalyst/upgrade-4f8715/) --- --- ## Related Pages - [To v1.19](https://img.ly/docs/cesdk/mac-catalyst/to-v1-19-55bcad/) - Learn what changed in v1.19 and how to update your implementation to stay compatible. --- ## 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 a Template" description: "Apply template scenes to an existing scene with the CE.SDK Engine API for Swift, preserving your page dimensions and design unit." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/use-templates/apply-template-35c73e/" --- > 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/) > [Apply a Template](https://img.ly/docs/cesdk/mac-catalyst/use-templates/apply-template-35c73e/) --- ```swift file=@cesdk_swift_examples/engine-guides-apply-template/ApplyTemplate.swift reference-only import Foundation import IMGLYEngine @MainActor func applyTemplate(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.appendChild(to: scene, child: page) try engine.block.setWidth(page, value: 1080) try engine.block.setHeight(page, value: 1920) let templateURL = baseURL .appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.applyTemplate(from: templateURL) guard let appliedPage = try engine.scene.getPages().first else { return } let width = try engine.block.getWidth(appliedPage) let height = try engine.block.getHeight(appliedPage) print("Page dimensions preserved: \(width) x \(height)") let alternativeTemplateURL = baseURL .appendingPathComponent("ly.img.templates/templates/cesdk_blank_1.scene") try await engine.scene.applyTemplate(from: alternativeTemplateURL) // A serialized scene string, here read from the template file. In production // it typically comes from your database or an API response. let templateData = try await URLSession.shared.data(from: templateURL).0 guard let templateString = String(bytes: templateData, encoding: .utf8) else { return } try await engine.scene.applyTemplate(from: templateString) } ``` Apply template content to an existing scene with the Swift Engine API while keeping your current page dimensions and design unit. Template content is automatically scaled to fit, so you can switch layouts without changing the canvas size. > **Reading time:** 5 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-apply-template) Applying a template loads the template's content into the current scene while keeping the scene's design unit and page dimensions. The content is resized to fit those dimensions. This differs from `scene.load(from:)`, which replaces the entire scene, including its dimensions. This guide covers applying templates from a URL and from a serialized string, verifying that page dimensions stay fixed, and switching between templates on the same scene. ## When to Use Apply vs Load Use `applyTemplate(from:)` when you want to: - **Switch templates**: Let users preview different templates while keeping a consistent canvas size. - **Standardize output dimensions**: Generate content with fixed sizes, such as social-media formats or print sizes. - **Batch process with templates**: Apply different templates to a pre-configured scene without dimension drift. Use `scene.load(from:)` when you need the template's original dimensions. **Key distinction**: Loading replaces everything, including dimensions; applying keeps your dimensions and resizes the template's content to fit. ## Apply a Template from URL Create a scene and set the page dimensions you want to keep. These dimensions are preserved when the template is applied. ```swift highlight-applyTemplate-setup 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: 1080) try engine.block.setHeight(page, value: 1920) ``` Call `applyTemplate(from:)` with the template's URL. The template's content is resized automatically to fit the current page dimensions. The method throws if no scene exists yet, so create or load one first. ```swift highlight-applyTemplate-fromURL let templateURL = baseURL .appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.applyTemplate(from: templateURL) ``` ## Verify Preserved Dimensions Applying a template reloads the scene, so query the current page again with `scene.getPages()` before reading its size. The width and height match the values set during setup, confirming the template adopted your dimensions instead of its own. ```swift highlight-applyTemplate-verifyDimensions guard let appliedPage = try engine.scene.getPages().first else { return } let width = try engine.block.getWidth(appliedPage) let height = try engine.block.getHeight(appliedPage) print("Page dimensions preserved: \(width) x \(height)") ``` ## Template Switching Apply another template to the same scene. Each call replaces the content while preserving the page dimensions and design unit, which is the basis for a "preview" experience where users explore different templates without affecting their canvas size. ```swift highlight-applyTemplate-switching let alternativeTemplateURL = baseURL .appendingPathComponent("ly.img.templates/templates/cesdk_blank_1.scene") try await engine.scene.applyTemplate(from: alternativeTemplateURL) ``` ## Apply a Template from String For templates stored in a database or returned from an API, pass the serialized scene contents to `applyTemplate(from:)` as a base64 string instead of a URL. The example reads the string from the template file; in your app it comes from your own storage. ```swift highlight-applyTemplate-fromString // A serialized scene string, here read from the template file. In production // it typically comes from your database or an API response. let templateData = try await URLSession.shared.data(from: templateURL).0 guard let templateString = String(bytes: templateData, encoding: .utf8) else { return } try await engine.scene.applyTemplate(from: templateString) ``` ## Troubleshooting ### No Scene Loaded `applyTemplate(from:)` requires an existing scene. Create one first with `engine.scene.create()` or load one with `engine.scene.load(from:)`; otherwise the call throws. ### Template Not Accessible Verify the template URL is reachable and valid. For remote URLs, check network connectivity; for bundled files, confirm the resource path resolves. ### Content Not Scaling as Expected Template content scales to fit the current page dimensions. Set the page dimensions before applying the template so the content adjusts to the size you expect. ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `scene.applyTemplate(from: URL)` | Scene | Apply a template from a URL, preserving current dimensions | | `scene.applyTemplate(from: String)` | Scene | Apply a template from a base64 string, preserving current dimensions | | `scene.create()` | Scene | Create a new scene as the target for template application | | `scene.getPages()` | Scene | Read the scene's pages to verify dimensions | | `block.create(.page)` | Block | Create a page block | | `block.appendChild(to:child:)` | Block | Add the page to the scene | | `block.setWidth(_:value:)` | Block | Set the page width | | `block.setHeight(_:value:)` | Block | Set the page height | | `block.getWidth(_:)` | Block | Read the page width | | `block.getHeight(_:)` | Block | Read the page height | ## Next Steps - [Templates Overview](https://img.ly/docs/cesdk/mac-catalyst/use-templates/overview-ae74e1/) — Understanding templates in CE.SDK - [Use Templates Programmatically](https://img.ly/docs/cesdk/mac-catalyst/use-templates/programmatic-9349f3/) — Comprehensive programmatic template workflows - [Generate From Template](https://img.ly/docs/cesdk/mac-catalyst/use-templates/generate-334e15/) — Generate finished designs by loading, populating, and exporting 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: "Generate From Template" description: "Generate finished designs from templates with the CE.SDK Engine API for Swift by loading, populating variables, and exporting to images and PDFs." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/use-templates/generate-334e15/" --- > 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/) > [Generate From Template](https://img.ly/docs/cesdk/mac-catalyst/use-templates/generate-334e15/) --- ```swift file=@cesdk_swift_examples/engine-guides-use-templates-generate/UseTemplatesGenerate.swift reference-only import Foundation import IMGLYEngine @MainActor func useTemplatesGenerate(engine: Engine) async throws { // Resolve sample assets against the engine's configured base URL. let baseURL = try engine.guidesBaseURL // Demo setup: build a small greeting-card template inline and serialize it so // the load call below has real template data to work with. In production your // templates come from the web editor or your storage — you load them the same // way, straight into `engine.scene.load(from:)`. let demoScene = try engine.scene.create(designUnit: .px) let demoPage = try engine.block.create(.page) try engine.block.setWidth(demoPage, value: 800) try engine.block.setHeight(demoPage, value: 600) try engine.block.appendChild(to: demoScene, child: demoPage) // An image placeholder with a semantic name the generation code looks up. let demoImage = try engine.block.create(.graphic) try engine.block.setShape(demoImage, shape: engine.block.createShape(.rect)) let demoFill = try engine.block.createFill(.image) try engine.block.setURL( demoFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(demoImage, fill: demoFill) try engine.block.setWidth(demoImage, value: 320) try engine.block.setHeight(demoImage, value: 320) try engine.block.setPositionX(demoImage, value: 60) try engine.block.setPositionY(demoImage, value: 140) try engine.block.setName(demoImage, name: "Image") try engine.block.setPlaceholderEnabled(demoImage, enabled: true) try engine.block.appendChild(to: demoPage, child: demoImage) // Two text blocks driven by variable tokens. let demoGreeting = try engine.block.create(.text) try engine.block.replaceText(demoGreeting, text: "Dear {{recipientName}},") try engine.block.setWidthMode(demoGreeting, mode: .auto) try engine.block.setHeightMode(demoGreeting, mode: .auto) try engine.block.setFloat(demoGreeting, property: "text/fontSize", value: 44) try engine.block.setPositionX(demoGreeting, value: 440) try engine.block.setPositionY(demoGreeting, value: 170) try engine.block.appendChild(to: demoPage, child: demoGreeting) let demoMessage = try engine.block.create(.text) try engine.block.replaceText(demoMessage, text: "{{message}}") try engine.block.setWidthMode(demoMessage, mode: .absolute) try engine.block.setWidth(demoMessage, value: 300) try engine.block.setHeightMode(demoMessage, mode: .auto) try engine.block.setFloat(demoMessage, property: "text/fontSize", value: 28) try engine.block.setPositionX(demoMessage, value: 440) try engine.block.setPositionY(demoMessage, value: 260) try engine.block.appendChild(to: demoPage, child: demoMessage) // Register the template's variables with default values. `findAll()` reports // registered variables, so a template must register a variable for each token // it defines — the CE.SDK editor does this automatically when you insert a // `{{token}}`. Registering them here serializes them with the scene. try engine.variable.set(key: "recipientName", value: "Friend") try engine.variable.set(key: "message", value: "Best wishes") let templateString = try await engine.scene.saveToString() // Load a template as the active scene. Pass `overrideEditorConfig: true` to // import the template's registered variables (and settings) into the engine. // Use a serialized string for stored data, a URL for a remote or bundled // `.scene` file with `engine.scene.load(from: URL)`, or // `engine.scene.load(from:)` for an archive that bundles its assets. try await engine.scene.load(from: templateString, overrideEditorConfig: true) // List the variables the template registers, and read a variable's value. let variableNames = engine.variable.findAll() let defaultRecipient = try engine.variable.get(key: "recipientName") print("Template variables:", variableNames, "— recipientName default:", defaultRecipient) // Assign values that replace the matching {{token}} placeholders in text. try engine.variable.set(key: "recipientName", value: "Alice") try engine.variable.set(key: "message", value: "Wishing you a wonderful year ahead!") // Discover every placeholder block, or look one up by its name. let placeholders = engine.block.findAllPlaceholders() print("Template placeholders:", placeholders.count) if let namedImage = engine.block.find(byName: "Image").first { print("Found image placeholder:", try engine.block.getName(namedImage)) } // Swap an image placeholder's source by updating its fill's image URI. if let imageBlock = engine.block.find(byName: "Image").first { let fill = try engine.block.getFill(imageBlock) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) } if let heroPage = try engine.block.find(byType: .page).first { try await engine.captureGuide(heroPage, label: "hero") } // Export the populated page to a PNG image at a target resolution. guard let page = try engine.block.find(byType: .page).first else { return } let pngData = try await engine.block.export( page, mimeType: .png, options: ExportOptions(targetWidth: 1920, targetHeight: 1080), ) print("Exported PNG:", pngData.count, "bytes") // Export the whole scene to a multi-page PDF document. if let scene = try engine.scene.get() { let pdfData = try await engine.block.export(scene, mimeType: .pdf) print("Exported PDF:", pdfData.count, "bytes") } // Personalize the same template once per record and export each result. let records: [[String: String]] = [ ["recipientName": "Alice", "message": "Wishing you a wonderful year ahead!"], ["recipientName": "Bob", "message": "Congratulations on the new home!"], ["recipientName": "Carol", "message": "Thank you for everything."], ] for record in records { // A reload with `overrideEditorConfig: true` re-imports the template's // own variables, resetting them to their serialized defaults each record. try await engine.scene.load(from: templateString, overrideEditorConfig: true) for (key, value) in record { try engine.variable.set(key: key, value: value) } guard let recordPage = try engine.block.find(byType: .page).first else { continue } let recordData = try await engine.block.export(recordPage, mimeType: .png) print("Exported \(record["recipientName"] ?? "record"):", recordData.count, "bytes") } } ``` Turn templates into finished designs with the Swift Engine API. Load a template, populate its variables and image placeholders with your own data, and export the result to a PNG or PDF — the workflow behind batch processing, personalization, and automated design production. ![A personalized greeting card generated from a template, with a photo and populated text.](./assets/swift-based.hero.webp) > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-use-templates-generate) Template generation transforms a template into a finished design by populating data and exporting to an output format. Load a template with `engine.scene.load(from:)`, replace its variables and placeholders with `engine.variable.set(key:value:)` and the block APIs, then export with `engine.block.export(_:mimeType:options:)`. This guide covers loading templates, populating variables, updating placeholder content, exporting to images and PDFs, and running batch generation. To merge a template into an existing scene while keeping your canvas size, see [Apply Templates](https://img.ly/docs/cesdk/mac-catalyst/use-templates/apply-template-35c73e/); for a deeper look at replacing content, see [Replace Content](https://img.ly/docs/cesdk/mac-catalyst/use-templates/replace-content-4c482b/). ## Loading Templates `engine.scene.load(from:)` accepts a serialized scene string, or a URL to a scene or archive file. Pass `overrideEditorConfig: true` to import the template's registered variables and settings into the engine, so you can discover and populate them after loading. The example loads a serialized template string it prepared from an inline demo scene. In your app, this string comes from your own storage or a template authored in the CE.SDK web editor. ```swift highlight-generate-load // Load a template as the active scene. Pass `overrideEditorConfig: true` to // import the template's registered variables (and settings) into the engine. // Use a serialized string for stored data, a URL for a remote or bundled // `.scene` file with `engine.scene.load(from: URL)`, or // `engine.scene.load(from:)` for an archive that bundles its assets. try await engine.scene.load(from: templateString, overrideEditorConfig: true) ``` ## Populating Variables Templates reference variables with `{{variableName}}` tokens in their text blocks, and register a variable for each token they define. Setting a variable replaces every matching token throughout the scene. ### Discover Available Variables List the variables a template registers with `engine.variable.findAll()` so you know which data it expects, and read a variable's current value with `engine.variable.get(key:)`. `findAll()` reports registered variables — a template loaded with `overrideEditorConfig: true` imports the variables it defines, and setting a variable registers it. Variable names are case-sensitive. ```swift highlight-generate-discoverVariables // List the variables the template registers, and read a variable's value. let variableNames = engine.variable.findAll() let defaultRecipient = try engine.variable.get(key: "recipientName") print("Template variables:", variableNames, "— recipientName default:", defaultRecipient) ``` ### Set Variable Values Assign a value to each variable with `engine.variable.set(key:value:)`. The matching `{{token}}` in the template's text updates immediately. ```swift highlight-generate-populateVariables // Assign values that replace the matching {{token}} placeholders in text. try engine.variable.set(key: "recipientName", value: "Alice") try engine.variable.set(key: "message", value: "Wishing you a wonderful year ahead!") ``` ## Updating Placeholder Content Beyond text variables, templates contain placeholder blocks for images and other content. Discover them with `engine.block.findAllPlaceholders()`, or look one up by its name with `engine.block.find(byName:)`. ```swift highlight-generate-findPlaceholders // Discover every placeholder block, or look one up by its name. let placeholders = engine.block.findAllPlaceholders() print("Template placeholders:", placeholders.count) if let namedImage = engine.block.find(byName: "Image").first { print("Found image placeholder:", try engine.block.getName(namedImage)) } ``` ### Update Image Placeholders Read a placeholder's fill with `engine.block.getFill(_:)`, then point its `fill/image/imageFileURI` property at your image URL to swap the content. ```swift highlight-generate-updateImage // Swap an image placeholder's source by updating its fill's image URI. if let imageBlock = engine.block.find(byName: "Image").first { let fill = try engine.block.getFill(imageBlock) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) } ``` ## Exporting to Images Render the populated design to an image with `engine.block.export(_:mimeType:options:)`. Find the page to export with `engine.block.find(byType: .page)`. ```swift highlight-generate-exportImage // Export the populated page to a PNG image at a target resolution. guard let page = try engine.block.find(byType: .page).first else { return } let pngData = try await engine.block.export( page, mimeType: .png, options: ExportOptions(targetWidth: 1920, targetHeight: 1080), ) print("Exported PNG:", pngData.count, "bytes") ``` Configure the output through `ExportOptions`: `targetWidth` and `targetHeight` set the render resolution, `pngCompressionLevel` (0–9) trades speed for smaller PNG files, and `jpegQuality` (0–1) sets JPEG quality. ## Exporting to PDF Export the whole scene to a PDF document with `mimeType: .pdf`. Exporting the scene rather than a single page includes every page in a multi-page document. ```swift highlight-generate-exportPDF // Export the whole scene to a multi-page PDF document. if let scene = try engine.scene.get() { let pdfData = try await engine.block.export(scene, mimeType: .pdf) print("Exported PDF:", pdfData.count, "bytes") } ``` ## Batch Generation Workflows Drive a single template with many data records. Serialize the template once with `engine.scene.saveToString()`, then loop over your records — reloading the template, populating variables, and exporting for each one. Reloading with `overrideEditorConfig: true` restores the template's own variables to their serialized defaults before each record, so their values don't leak between exports. Loading merges variables rather than clearing them, so a variable you set that the template doesn't define persists across reloads — remove it with `engine.variable.remove(key:)` if a later record shouldn't inherit it. ```swift highlight-generate-batch // Personalize the same template once per record and export each result. let records: [[String: String]] = [ ["recipientName": "Alice", "message": "Wishing you a wonderful year ahead!"], ["recipientName": "Bob", "message": "Congratulations on the new home!"], ["recipientName": "Carol", "message": "Thank you for everything."], ] for record in records { // A reload with `overrideEditorConfig: true` re-imports the template's // own variables, resetting them to their serialized defaults each record. try await engine.scene.load(from: templateString, overrideEditorConfig: true) for (key, value) in record { try engine.variable.set(key: key, value: value) } guard let recordPage = try engine.block.find(byType: .page).first else { continue } let recordData = try await engine.block.export(recordPage, mimeType: .png) print("Exported \(record["recipientName"] ?? "record"):", recordData.count, "bytes") } ``` ## Troubleshooting ### Template Fails to Load Confirm the URL is reachable and returns valid scene data, and that the scene format is compatible with your SDK version. Wrap load calls in `do`/`catch` to handle network or parsing errors. ### Variables Not Updating Ensure the variable name passed to `engine.variable.set(key:value:)` exactly matches the `{{token}}` in the template. Names are case-sensitive. Use `engine.variable.findAll()` to list the registered variable names. ### Export Returns Empty Output Confirm every referenced asset is reachable and that blocks are attached to the page hierarchy — orphaned blocks do not appear in exports. ### Image Placeholder Not Found Verify the name passed to `engine.block.find(byName:)` matches the block's name exactly. Names are case-sensitive. Use `engine.block.findAllPlaceholders()` to discover every placeholder in the scene. ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `scene.load(from: String)` | Scene | Load a template from a serialized string | | `scene.load(from: URL)` | Scene | Load a template scene or archive from a remote or bundled URL (kind detected automatically) | | `scene.saveToString()` | Scene | Serialize the scene for batch processing | | `scene.get()` | Scene | Get the active scene block | | `variable.findAll()` | Variable | List every registered variable in the scene | | `variable.set(key:value:)` | Variable | Set (and register) a variable value | | `variable.get(key:)` | Variable | Read a variable value | | `variable.remove(key:)` | Variable | Remove a registered variable | | `block.find(byName:)` | Block | Find blocks by name | | `block.find(byType:)` | Block | Find blocks by type, such as `.page` | | `block.findAllPlaceholders()` | Block | Discover all placeholder blocks | | `block.getFill(_:)` | Block | Get a block's fill | | `block.setURL(_:property:value:)` | Block | Set a URL property, such as an image fill URI | | `block.export(_:mimeType:options:)` | Block | Export a block to a PNG, JPEG, or PDF blob | ## Next Steps - [Templates Overview](https://img.ly/docs/cesdk/mac-catalyst/use-templates/overview-ae74e1/) — Understand how templates work in CE.SDK - [Apply Templates](https://img.ly/docs/cesdk/mac-catalyst/use-templates/apply-template-35c73e/) — Merge a template into an existing scene while preserving its dimensions - [Replace Content](https://img.ly/docs/cesdk/mac-catalyst/use-templates/replace-content-4c482b/) — Update variables and placeholders in depth - [Use Templates Programmatically](https://img.ly/docs/cesdk/mac-catalyst/use-templates/programmatic-9349f3/) — Build and personalize templates entirely in code --- ## 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: "Template Library" description: "Learn how to provide a set of predefined templates in the CreativeEditor SDK." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/use-templates/library-b3c704/" --- > 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/) > [Template Library](https://img.ly/docs/cesdk/mac-catalyst/use-templates/library-b3c704/) --- ```swift file=@cesdk_swift_examples/engine-guides-use-templates-library/TemplateLibrary.swift reference-only import Foundation import IMGLYEngine @MainActor func templateLibrary(engine: Engine) async throws { // Create a design scene that templates will be applied to. 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) // Base URL the sample templates are resolved against. In your app this is the // location where you host your own `.scene` files and thumbnails. let baseURL = try engine.guidesBaseURL // Register a local template source. The `applyAsset` callback runs when a // template is selected: it reads the scene URL from the asset's metadata and // applies it to the current scene, keeping the current page dimensions. try engine.asset.addLocalSource(sourceID: "my.custom.templates", applyAsset: { [weak engine] asset in guard let engine, let uri = asset.meta?["uri"], let sceneURL = URL(string: uri) else { return nil } try await engine.scene.applyTemplate(from: sceneURL) return nil }) // Add template assets. Each asset's `meta` carries the `uri` of the `.scene` // file to apply and a `thumbUri` for the preview thumbnail. try engine.asset.addAsset( to: "my.custom.templates", asset: AssetDefinition( id: "business-card", groups: ["business"], 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"], tags: ["en": ["business", "card"]], ), ) try engine.asset.addAsset( to: "my.custom.templates", asset: AssetDefinition( id: "blank-canvas", groups: ["basics"], 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 Canvas"], tags: ["en": ["blank", "empty"]], ), ) // Apply a template by running the source's callback, exactly as the editor // does when a user taps a template thumbnail. if let firstTemplate = try await engine.asset.findAssets( sourceID: "my.custom.templates", query: .init(query: nil, page: 0, perPage: 1), ).assets.first { _ = try await engine.asset.apply(sourceID: "my.custom.templates", assetResult: firstTemplate) } // For production, register a template source from a hosted `content.json` // file. The parent directory of the JSON becomes the base path for resolving // relative URLs inside it. let contentURL = baseURL.appendingPathComponent("ly.img.templates/content.json") let hostedSourceID = try await engine.asset.addLocalAssetSourceFromJSON(contentURL) print("Registered hosted template source:", hostedSourceID) // Query templates with pagination and group filtering. let businessTemplates = try await engine.asset.findAssets( sourceID: "my.custom.templates", query: .init(query: nil, page: 0, groups: ["business"], perPage: 20), ) print("Templates in \"business\" group:", businessTemplates.assets.map(\.id)) let allTemplates = try await engine.asset.findAssets( sourceID: "my.custom.templates", query: .init(query: nil, page: 0, perPage: 100), ) print("Total custom templates:", allTemplates.total) // List registered sources, read a source's groups, and remove a source. let templateSources = engine.asset.findAllSources().filter { $0.contains("template") } print("Template sources:", templateSources) let groups = try await engine.asset.getGroups(sourceID: "my.custom.templates") print("Available groups:", groups) try engine.asset.removeSource(sourceID: hostedSourceID) // React to sources being added or removed. let addedTask = Task { for await sourceID in engine.asset.onAssetSourceAdded { print("Asset source added:", sourceID) break } } try engine.asset.addLocalSource(sourceID: "seasonal.templates") addedTask.cancel() let removedTask = Task { for await sourceID in engine.asset.onAssetSourceRemoved { print("Asset source removed:", sourceID) break } } try engine.asset.removeSource(sourceID: "seasonal.templates") removedTask.cancel() } ``` Configure and populate a Template Library with the Swift Engine API so your app can offer predefined design templates that users browse, select, and apply to the current scene. > **Reading time:** 6 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-use-templates-library) Templates are pre-designed scenes stored as assets within an asset source. Each template asset keeps the URL of its `.scene` file in its metadata, and an apply callback loads that scene into the current design when the template is selected. This makes templates different from image or sticker sources: instead of instantiating a single block, a template applies a complete scene. This guide covers creating a custom template source, handling template application, registering templates from a hosted JSON file, querying templates, and managing template sources. ## Setup Create a design scene for templates to be applied to. Applying a template keeps the current scene's design unit and page dimensions, adjusting the template's content to fit. ```swift highlight-templateLibrary-setup // Create a design scene that templates will be applied to. 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) ``` ## Creating Custom Template Sources Register a local source with `addLocalSource(sourceID:applyAsset:)`. The `applyAsset` callback runs when a template is selected: it reads the scene URL from the asset's `meta["uri"]` and applies it with `engine.scene.applyTemplate(from:)`. Return `nil` because applying a template mutates the current scene rather than creating a new block. The source retains this callback for its lifetime, so capture `engine` weakly to avoid a retain cycle. ```swift highlight-templateLibrary-customSource // Register a local template source. The `applyAsset` callback runs when a // template is selected: it reads the scene URL from the asset's metadata and // applies it to the current scene, keeping the current page dimensions. try engine.asset.addLocalSource(sourceID: "my.custom.templates", applyAsset: { [weak engine] asset in guard let engine, let uri = asset.meta?["uri"], let sceneURL = URL(string: uri) else { return nil } try await engine.scene.applyTemplate(from: sceneURL) return nil }) ``` Add template assets with `addAsset(to:asset:)`. Build each asset's `meta` URLs from the base URL where you host your `.scene` files and thumbnails. Every `AssetDefinition` can include: - `id` — Unique identifier for the template. - `label` — Localized display name, for example `["en": "Business Card"]`. - `tags` — Localized keywords used for free-text search. - `groups` — Categories used for filtering. - `meta["uri"]` — URL of the `.scene` file to apply. Required for template application. - `meta["thumbUri"]` — Thumbnail image URL shown in previews. ```swift highlight-templateLibrary-addAssets // Add template assets. Each asset's `meta` carries the `uri` of the `.scene` // file to apply and a `thumbUri` for the preview thumbnail. try engine.asset.addAsset( to: "my.custom.templates", asset: AssetDefinition( id: "business-card", groups: ["business"], 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"], tags: ["en": ["business", "card"]], ), ) try engine.asset.addAsset( to: "my.custom.templates", asset: AssetDefinition( id: "blank-canvas", groups: ["basics"], 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 Canvas"], tags: ["en": ["blank", "empty"]], ), ) ``` ### From Remote URI For production, register a template source from a hosted `content.json` file with `addLocalAssetSourceFromJSON(_:)`. The parent directory of the JSON URL becomes the base path for resolving relative URLs inside it, so you can host templates and thumbnails alongside the JSON. ```swift highlight-templateLibrary-fromJSON // For production, register a template source from a hosted `content.json` // file. The parent directory of the JSON becomes the base path for resolving // relative URLs inside it. let contentURL = baseURL.appendingPathComponent("ly.img.templates/content.json") let hostedSourceID = try await engine.asset.addLocalAssetSourceFromJSON(contentURL) print("Registered hosted template source:", hostedSourceID) ``` ## Querying Templates Programmatically Search a source with `findAssets(sourceID:query:)`. Filter by `groups`, search free text with `query`, and page through results with `page` and `perPage`. ```swift highlight-templateLibrary-query // Query templates with pagination and group filtering. let businessTemplates = try await engine.asset.findAssets( sourceID: "my.custom.templates", query: .init(query: nil, page: 0, groups: ["business"], perPage: 20), ) print("Templates in \"business\" group:", businessTemplates.assets.map(\.id)) let allTemplates = try await engine.asset.findAssets( sourceID: "my.custom.templates", query: .init(query: nil, page: 0, perPage: 100), ) print("Total custom templates:", allTemplates.total) ``` The returned `AssetQueryResult` exposes the matching `assets`, the `currentPage`, the `nextPage` (`-1` when there is no further page), and the `total` count across all pages. ## Managing Template Sources List sources with `findAllSources()`, read a source's categories with `getGroups(sourceID:)`, and remove a source with `removeSource(sourceID:)`. ```swift highlight-templateLibrary-manageSources // List registered sources, read a source's groups, and remove a source. let templateSources = engine.asset.findAllSources().filter { $0.contains("template") } print("Template sources:", templateSources) let groups = try await engine.asset.getGroups(sourceID: "my.custom.templates") print("Available groups:", groups) try engine.asset.removeSource(sourceID: hostedSourceID) ``` Subscribe to `onAssetSourceAdded` and `onAssetSourceRemoved` to react when sources change. Both are `AsyncStream` sequences that yield the affected source ID. ```swift highlight-templateLibrary-monitorSources // React to sources being added or removed. let addedTask = Task { for await sourceID in engine.asset.onAssetSourceAdded { print("Asset source added:", sourceID) break } } try engine.asset.addLocalSource(sourceID: "seasonal.templates") addedTask.cancel() let removedTask = Task { for await sourceID in engine.asset.onAssetSourceRemoved { print("Asset source removed:", sourceID) break } } try engine.asset.removeSource(sourceID: "seasonal.templates") removedTask.cancel() ``` ## Troubleshooting - **Templates not appearing**: Confirm the source is registered by checking that its ID is returned by `findAllSources()`. - **Templates not applying**: The apply callback returns `nil` when an asset is missing `meta["uri"]`, so a misconfigured template is skipped instead of crashing. Verify each asset's `uri` points to a `.scene` file your app can load. - **Thumbnails not loading**: Verify each `meta["thumbUri"]` is a reachable URL. ## API Reference | Method | Category | Purpose | | --------------------------------------------------- | -------- | ---------------------------------------------- | | `engine.asset.addLocalSource(sourceID:applyAsset:)` | Asset | Create a source with a template apply callback | | `engine.asset.addAsset(to:asset:)` | Asset | Add a template asset to a source | | `engine.scene.applyTemplate(from:)` | Scene | Apply a template scene to the current scene | | `engine.asset.addLocalAssetSourceFromJSON(_:)` | Asset | Register a source from a hosted JSON file | | `engine.asset.findAssets(sourceID:query:)` | Asset | Query a source with filtering and pagination | | `engine.asset.findAllSources()` | Asset | List the IDs of all registered sources | | `engine.asset.getGroups(sourceID:)` | Asset | Read the available groups of a source | | `engine.asset.removeSource(sourceID:)` | Asset | Remove a source | | `engine.asset.onAssetSourceAdded` | Asset | Stream of IDs for sources as they are added | | `engine.asset.onAssetSourceRemoved` | Asset | Stream of IDs for sources as they are removed | ## Next Steps - [Apply Templates](https://img.ly/docs/cesdk/mac-catalyst/use-templates/apply-template-35c73e/) — Apply a template scene to the current design from a string or URL. - [Asset Sources](https://img.ly/docs/cesdk/mac-catalyst/concepts/assets-a84fdd/) — Understand how asset sources organize and deliver content. - [Serve Assets](https://img.ly/docs/cesdk/mac-catalyst/serve-assets-b0827c/) — Host your templates, scenes, and thumbnails for production. --- ## 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 browse, apply, and dynamically populate templates in CE.SDK to streamline design workflows." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/use-templates/overview-ae74e1/" --- > 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/) > [Use Templates Overview](https://img.ly/docs/cesdk/mac-catalyst/use-templates/overview-ae74e1/) --- Templates in CreativeEditor SDK (CE.SDK) are pre-designed layouts that serve as starting points for generating static designs, videos, or print-ready outputs. Templates can be used to produce a wide range of media, including images, PDFs, and videos. Instead of creating a design from scratch, you can use a template to quickly produce content by adapting pre-defined elements like text, images, and layout structures. Using templates offers significant advantages: faster content creation, consistent visual style, and scalable design workflows across many outputs. CE.SDK supports two modes of using templates: - **Fully Programmatic**: Generate content variations automatically by merging external data into templates without user intervention. - **User-Assisted**: Let users load a template, customize editable elements, and export the result manually. Template-based generation can be performed entirely on the client, entirely on a server, or in a hybrid setup where users interact with templates client-side before triggering automated server-side generation. [Explore Demos](https://img.ly/showcases/cesdk?tags=ios) [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) ## Output Formats When Using Templates When generating outputs from templates, CE.SDK supports: Templates are format-aware, allowing you to design once and export to multiple formats seamlessly. For example, a single marketing template could be used to produce a social media graphic, a printable flyer, and a promotional video, all using the same underlying design. --- ## 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: "Use Templates Programmatically" description: "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." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/use-templates/programmatic-9349f3/" --- > 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/) > [Programmatic](https://img.ly/docs/cesdk/mac-catalyst/use-templates/programmatic-9349f3/) --- ```swift file=@cesdk_swift_examples/engine-guides-use-templates-programmatic/UseTemplatesProgrammatically.swift reference-only import Foundation import IMGLYEngine @MainActor func useTemplatesProgrammatically(engine: Engine) async throws { // Resolve sample assets against the engine's configured base URL. let baseURL = try engine.guidesBaseURL let outputDir = FileManager.default.temporaryDirectory // Build a greeting card template from scratch. let scene = try engine.scene.create() 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: 800) try engine.block.setHeight(page, value: 600) // Light gray page background. let pageFill = try engine.block.getFill(page) try engine.block.setColor(pageFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.95, b: 0.95, a: 1)) // Define the variables before any text references them. try engine.variable.set(key: "recipientName", value: "Template") try engine.variable.set(key: "customMessage", value: "This is a template example") // Title text block with a variable token. let titleBlock = try engine.block.create(.text) try engine.block.setName(titleBlock, name: "title") try engine.block.appendChild(to: page, child: titleBlock) try engine.block.setPositionX(titleBlock, value: 50) try engine.block.setPositionY(titleBlock, value: 50) try engine.block.setWidth(titleBlock, value: 700) try engine.block.setHeight(titleBlock, value: 80) try engine.block.replaceText(titleBlock, text: "Hello, {{recipientName}}!") try engine.block.setTextColor(titleBlock, color: .rgba(r: 0.2, g: 0.2, b: 0.2, a: 1)) try engine.block.setFloat(titleBlock, property: "text/fontSize", value: 48) // Message text block with a variable token. let messageBlock = try engine.block.create(.text) try engine.block.setName(messageBlock, name: "message") try engine.block.appendChild(to: page, child: messageBlock) try engine.block.setPositionX(messageBlock, value: 50) try engine.block.setPositionY(messageBlock, value: 140) try engine.block.setWidth(messageBlock, value: 700) try engine.block.setHeight(messageBlock, value: 120) try engine.block.replaceText(messageBlock, text: "{{customMessage}}") try engine.block.setTextColor(messageBlock, color: .rgba(r: 0.3, g: 0.3, b: 0.3, a: 1)) try engine.block.setFloat(messageBlock, property: "text/fontSize", value: 28) // List every variable the template defines and read a current value. let variableNames = engine.variable.findAll() print("Template variables:", variableNames) let titleUsesVariables = try engine.block.referencesAnyVariables(titleBlock) print("Title references variables:", titleUsesVariables) let currentName = try engine.variable.get(key: "recipientName") print("recipientName =", currentName) // Serialize the template to a string and persist it for reuse. let templateString = try await engine.scene.saveToString() let templateURL = outputDir.appendingPathComponent("template.imgly") try templateString.write(to: templateURL, atomically: true, encoding: .utf8) // Populate the template with each record and export a personalized card. let recipients = [ (name: "Alice", message: "Congratulations on your promotion!"), (name: "Bob", message: "Happy Birthday! Have a wonderful day!"), (name: "Charlie", message: "Thank you for your amazing work!"), ] for recipient in recipients { try engine.variable.set(key: "recipientName", value: recipient.name) try engine.variable.set(key: "customMessage", value: recipient.message) let cardData = try await engine.block.export( page, mimeType: .png, options: ExportOptions(targetWidth: 800, targetHeight: 600), ) let filename = "greeting-card-\(recipient.name.lowercased()).png" try cardData.write(to: outputDir.appendingPathComponent(filename)) } // Reload the saved template before each record so prior edits never carry over. let records = [ (name: "Diana", message: "Welcome to the team!"), (name: "Eve", message: "Great work this quarter!"), ] for record in records { try await engine.scene.load(from: templateString) guard let recordPage = try engine.block.find(byType: .page).first else { continue } try engine.variable.set(key: "recipientName", value: record.name) try engine.variable.set(key: "customMessage", value: record.message) let recordData = try await engine.block.export(recordPage, mimeType: .png) try recordData.write(to: outputDir.appendingPathComponent("record-\(record.name.lowercased()).png")) } // Variable keys are case-sensitive and persist with the scene. Removing a // variable leaves its literal token in any text that still references it. try engine.variable.remove(key: "customMessage") print("Variables after removal:", engine.variable.findAll()) // Load a pre-built template from a URL. This replaces the current scene with // the template's pages, blocks, variables, and placeholders. let templateAssetURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: templateAssetURL) } ``` Automate template workflows with CE.SDK's Swift Engine API for batch processing, personalization, and headless design generation. > **Reading time:** 10 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-use-templates-programmatic) Templates are scenes with predefined structures that support dynamic content through variables. This guide shows you how to work with them through the Engine API—no editor interface required. You build a greeting card template from scratch, bind variables, save it for reuse, and batch-export personalized designs. ## Creating Templates from Scratch Build a template by creating a scene with `engine.scene.create()`, then arranging blocks with `engine.block.create(_:)` and `engine.block.appendChild(to:child:)`. ```swift highlight-useTemplatesProgrammatic-createTemplate // Build a greeting card template from scratch. let scene = try engine.scene.create() 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: 800) try engine.block.setHeight(page, value: 600) // Light gray page background. let pageFill = try engine.block.getFill(page) try engine.block.setColor(pageFill, property: "fill/color/value", color: .rgba(r: 0.95, g: 0.95, b: 0.95, a: 1)) // Define the variables before any text references them. try engine.variable.set(key: "recipientName", value: "Template") try engine.variable.set(key: "customMessage", value: "This is a template example") // Title text block with a variable token. let titleBlock = try engine.block.create(.text) try engine.block.setName(titleBlock, name: "title") try engine.block.appendChild(to: page, child: titleBlock) try engine.block.setPositionX(titleBlock, value: 50) try engine.block.setPositionY(titleBlock, value: 50) try engine.block.setWidth(titleBlock, value: 700) try engine.block.setHeight(titleBlock, value: 80) try engine.block.replaceText(titleBlock, text: "Hello, {{recipientName}}!") try engine.block.setTextColor(titleBlock, color: .rgba(r: 0.2, g: 0.2, b: 0.2, a: 1)) try engine.block.setFloat(titleBlock, property: "text/fontSize", value: 48) // Message text block with a variable token. let messageBlock = try engine.block.create(.text) try engine.block.setName(messageBlock, name: "message") try engine.block.appendChild(to: page, child: messageBlock) try engine.block.setPositionX(messageBlock, value: 50) try engine.block.setPositionY(messageBlock, value: 140) try engine.block.setWidth(messageBlock, value: 700) try engine.block.setHeight(messageBlock, value: 120) try engine.block.replaceText(messageBlock, text: "{{customMessage}}") try engine.block.setTextColor(messageBlock, color: .rgba(r: 0.3, g: 0.3, b: 0.3, a: 1)) try engine.block.setFloat(messageBlock, property: "text/fontSize", value: 28) ``` The title block contains `Hello, {{recipientName}}!` and the message block contains `{{customMessage}}`. The double-brace tokens mark where variables get substituted. We size and position each block, give the page a light gray background, and use a larger font size for the title than the message. ## Text Variables for Dynamic Content Variables drive text replacement throughout a template. Define them with `engine.variable.set(key:value:)`; any text containing the matching `{{key}}` updates automatically. Read a current value with `engine.variable.get(key:)`, list every key with `engine.variable.findAll()`, and confirm a block uses variables with `engine.block.referencesAnyVariables(_:)`. ```swift highlight-useTemplatesProgrammatic-manageVariables // List every variable the template defines and read a current value. let variableNames = engine.variable.findAll() print("Template variables:", variableNames) let titleUsesVariables = try engine.block.referencesAnyVariables(titleBlock) print("Title references variables:", titleUsesVariables) let currentName = try engine.variable.get(key: "recipientName") print("recipientName =", currentName) ``` ## Populating Template Content Updating a variable refreshes every text block that references it—there's no need to find and edit individual blocks. Map external data fields (JSON, API responses, database rows) onto variable keys to populate a template in a single step. For swappable media, use placeholder blocks; see [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/). ## Saving Templates for Reuse Serialize a template to a portable string with `engine.scene.saveToString()`. The result is a base64-encoded scene containing every block, property, and variable definition—store it in a database or write it to disk. ```swift highlight-useTemplatesProgrammatic-saveTemplate // Serialize the template to a string and persist it for reuse. let templateString = try await engine.scene.saveToString() let templateURL = outputDir.appendingPathComponent("template.imgly") try templateString.write(to: templateURL, atomically: true, encoding: .utf8) ``` To bundle a template together with its assets, such as images and fonts, use `engine.scene.saveToArchive()` instead. ## Batch Processing with Templates Batch processing populates one template with many records. We update the variables for each recipient and export a personalized image with `engine.block.export(_:mimeType:options:)`. ```swift highlight-useTemplatesProgrammatic-batchProcessing // Populate the template with each record and export a personalized card. let recipients = [ (name: "Alice", message: "Congratulations on your promotion!"), (name: "Bob", message: "Happy Birthday! Have a wonderful day!"), (name: "Charlie", message: "Thank you for your amazing work!"), ] for recipient in recipients { try engine.variable.set(key: "recipientName", value: recipient.name) try engine.variable.set(key: "customMessage", value: recipient.message) let cardData = try await engine.block.export( page, mimeType: .png, options: ExportOptions(targetWidth: 800, targetHeight: 600), ) let filename = "greeting-card-\(recipient.name.lowercased()).png" try cardData.write(to: outputDir.appendingPathComponent(filename)) } ``` `ExportOptions(targetWidth:targetHeight:)` controls the output resolution. Each call returns the rendered image data, which we write to disk. ## Data-Driven Workflows When populating a template mutates state you don't want to carry between records, reload the saved string before each one. `engine.scene.load(from:)` restores the template to its saved state, then you apply that record's data and export. ```swift highlight-useTemplatesProgrammatic-dataDriven // Reload the saved template before each record so prior edits never carry over. let records = [ (name: "Diana", message: "Welcome to the team!"), (name: "Eve", message: "Great work this quarter!"), ] for record in records { try await engine.scene.load(from: templateString) guard let recordPage = try engine.block.find(byType: .page).first else { continue } try engine.variable.set(key: "recipientName", value: record.name) try engine.variable.set(key: "customMessage", value: record.message) let recordData = try await engine.block.export(recordPage, mimeType: .png) try recordData.write(to: outputDir.appendingPathComponent("record-\(record.name.lowercased()).png")) } ``` This pattern powers personalized certificates, greeting cards, and social media graphics generated from a single design. ## Managing Variables Variable keys are case-sensitive and persist with the scene. Remove one with `engine.variable.remove(key:)` when it's no longer needed; any text that still references it keeps the literal `{{token}}`. ```swift highlight-useTemplatesProgrammatic-removeVariable // Variable keys are case-sensitive and persist with the scene. Removing a // variable leaves its literal token in any text that still references it. try engine.variable.remove(key: "customMessage") print("Variables after removal:", engine.variable.findAll()) ``` ## Loading Existing Templates Templates don't have to be built in code. Load a pre-built scene from a URL with `engine.scene.load(from:)`. This replaces the current scene with the template's pages, blocks, variables, and placeholders. ```swift highlight-useTemplatesProgrammatic-loadExisting // Load a pre-built template from a URL. This replaces the current scene with // the template's pages, blocks, variables, and placeholders. let templateAssetURL = baseURL.appendingPathComponent("ly.img.templates/templates/cesdk_business_card_1.scene") try await engine.scene.load(from: templateAssetURL) ``` For templates bundled with their assets, `engine.scene.load(from:)` loads the complete package. To merge a template into the current scene instead of replacing it, use `engine.scene.applyTemplate(from:)`. ## API Reference | Method | Description | | --- | --- | | `engine.scene.create()` | Create a blank scene to build a template | | `engine.block.create(_:)` | Create a new design block | | `engine.block.appendChild(to:child:)` | Add a block to the scene hierarchy | | `engine.block.setPositionX(_:value:)` | Set a block's horizontal position | | `engine.block.setPositionY(_:value:)` | Set a block's vertical position | | `engine.block.setWidth(_:value:)` | Set a block's width | | `engine.block.setHeight(_:value:)` | Set a block's height | | `engine.block.replaceText(_:text:)` | Set text content, including `{{variable}}` tokens | | `engine.block.referencesAnyVariables(_:)` | Check whether a block uses variables | | `engine.block.find(byType:)` | Find blocks of a given type, such as `.page` | | `engine.variable.set(key:value:)` | Create or update a text variable | | `engine.variable.get(key:)` | Read a variable's current value | | `engine.variable.findAll()` | List every variable key in the scene | | `engine.variable.remove(key:)` | Delete a variable from the scene | | `engine.scene.saveToString()` | Serialize the scene to a portable string | | `engine.scene.load(from:)` | Load a scene from a string or URL | | `engine.block.export(_:mimeType:options:)` | Export a block to image data | ## Troubleshooting **Template loading failures:** Verify scene strings are correctly encoded and URLs are reachable. Wrap loading calls in `do`/`catch` to handle network or parsing errors. **Variables not replacing text:** Variable keys inside `{{}}` must exactly match the keys passed to `engine.variable.set(key:value:)`. Keys are case-sensitive. **Export issues:** Confirm all required assets are reachable before exporting. Missing images or fonts cause export failures. Check the block hierarchy—orphaned blocks that aren't connected to the page tree don't appear in exports. ## Next Steps - [Create From Scratch](https://img.ly/docs/cesdk/mac-catalyst/create-templates/from-scratch-663cda/) — Build reusable template structures block by block. - [Text Variables](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/text-variables-7ecb50/) — Define dynamic text elements populated at runtime. - [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) — Mark editable image, video, and text areas in a locked layout. - [Automate Design Generation](https://img.ly/docs/cesdk/mac-catalyst/automation/design-generation-98a99e/) — Generate on-brand designs programmatically from templates. - [Save](https://img.ly/docs/cesdk/mac-catalyst/export-save-publish/save-c8b124/) — Persist designs locally or to a backend for later editing. - [Data Merge](https://img.ly/docs/cesdk/mac-catalyst/automation/data-merge-ae087c/) — Generate personalized designs by merging external data into 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: "Replace Content" description: "Dynamically replace text, images, and placeholder content within templates using CE.SDK's placeholder and variable systems." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/use-templates/replace-content-4c482b/" --- > 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/) > [Replace Content](https://img.ly/docs/cesdk/mac-catalyst/use-templates/replace-content-4c482b/) --- ```swift file=@cesdk_swift_examples/engine-guides-replace-content/ReplaceContent.swift reference-only import Foundation import IMGLYEngine @MainActor func replaceContent(engine: Engine) async throws { // Resolve sample assets against the engine's configured base URL. let baseURL = try engine.guidesBaseURL // Demo setup: build a minimal template inline so the replacement APIs below // have named placeholders and variables to operate on. 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) // An image placeholder with a semantic name. let productImage = try engine.block.create(.graphic) try engine.block.setShape(productImage, shape: engine.block.createShape(.rect)) let productFill = try engine.block.createFill(.image) try engine.block.setURL( productFill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"), ) try engine.block.setFill(productImage, fill: productFill) try engine.block.setWidth(productImage, value: 300) try engine.block.setHeight(productImage, value: 300) try engine.block.setPositionX(productImage, value: 50) try engine.block.setPositionY(productImage, value: 50) try engine.block.setName(productImage, name: "product-image") try engine.block.setPlaceholderEnabled(productImage, enabled: true) try engine.block.appendChild(to: page, child: productImage) // A text block driven by a variable token. let headline = try engine.block.create(.text) try engine.block.replaceText(headline, text: "{{headline}}") try engine.block.setWidthMode(headline, mode: .auto) try engine.block.setHeightMode(headline, mode: .auto) try engine.block.setFloat(headline, property: "text/fontSize", value: 48) try engine.block.setPositionX(headline, value: 400) try engine.block.setPositionY(headline, value: 120) try engine.block.setName(headline, name: "headline") try engine.block.appendChild(to: page, child: headline) // A plain text block updated through direct replacement. let subtitle = try engine.block.create(.text) try engine.block.replaceText(subtitle, text: "Original subtitle") try engine.block.setWidthMode(subtitle, mode: .auto) try engine.block.setHeightMode(subtitle, mode: .auto) try engine.block.setFloat(subtitle, property: "text/fontSize", value: 24) try engine.block.setPositionX(subtitle, value: 400) try engine.block.setPositionY(subtitle, value: 220) try engine.block.setName(subtitle, name: "subtitle") try engine.block.appendChild(to: page, child: subtitle) // Find a specific block when you know its name. Names are case-sensitive. let headlineBlock = engine.block.find(byName: "headline").first if let headlineBlock { print("Found block named:", try engine.block.getName(headlineBlock)) } // Discover every placeholder block so you can iterate over them. let placeholders = engine.block.findAllPlaceholders() print("Template placeholders:", placeholders.count) if let imageBlock = engine.block.find(byName: "product-image").first { let fill = try engine.block.getFill(imageBlock) if try engine.block.supportsPlaceholderBehavior(fill) { let enabled = try engine.block.isPlaceholderEnabled(imageBlock) print("product-image placeholder enabled:", enabled) } } // The headline block contains "{{headline}}" and updates when the variable is set. try engine.variable.set(key: "headline", value: "Summer Sale") // List every variable the template references and read a current value. let variableNames = engine.variable.findAll() let headlineValue = try engine.variable.get(key: "headline") print("Variables:", variableNames, "headline =", headlineValue) // Remove a variable you no longer need. try engine.variable.set(key: "legacyTag", value: "obsolete") try engine.variable.remove(key: "legacyTag") // Swap an image placeholder's source by updating its fill's image URI. if let imageBlock = engine.block.find(byName: "product-image").first { let fill = try engine.block.getFill(imageBlock) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) } // Replace the full text of a block without using the variable system. if let subtitleBlock = engine.block.find(byName: "subtitle").first { try engine.block.replaceText(subtitleBlock, text: "Up to 50% off this week") } // Populate the template once per record, then export each result. let records: [[String: String]] = [ ["headline": "Summer Sale", "subtitle": "Up to 50% off", "image": "ly.img.image/images/sample_1.jpg"], ["headline": "Winter Sale", "subtitle": "Cozy deals inside", "image": "ly.img.image/images/sample_2.jpg"], ] for record in records { if let headline = record["headline"] { try engine.variable.set(key: "headline", value: headline) } if let subtitle = record["subtitle"], let subtitleBlock = engine.block.find(byName: "subtitle").first { try engine.block.replaceText(subtitleBlock, text: subtitle) } if let imagePath = record["image"], let imageBlock = engine.block.find(byName: "product-image").first { let fill = try engine.block.getFill(imageBlock) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent(imagePath), ) } let blob = try await engine.block.export(page, mimeType: .png) print("Exported \(record["headline"] ?? "record"):", blob.count, "bytes") } } ``` Dynamically replace content within templates using CE.SDK's placeholder and variable systems. Find placeholder blocks by name, update text using variables, and swap image sources programmatically. > **Reading time:** 7 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-replace-content) Template content replacement enables dynamic designs by swapping placeholder content programmatically. Templates contain blocks marked as placeholders that can be located by name or discovered in bulk for batch processing. Text replacement uses the variable system with `{{variableName}}` syntax, while images are updated by modifying fill properties. This guide covers how to find placeholder blocks, replace text using variables, swap image content, and build data-driven template workflows. It assumes you already have a template loaded — see [Templating](https://img.ly/docs/cesdk/mac-catalyst/concepts/templating-f94385/) for the model behind variables and placeholders. ## Finding Placeholder Blocks Locate replaceable content with block discovery APIs. Use `find(byName:)` to find specific blocks when you know the placeholder name. Names are case-sensitive, and `getName(_:)` reports the name set on a block. ```swift highlight-replaceContent-findByName // Find a specific block when you know its name. Names are case-sensitive. let headlineBlock = engine.block.find(byName: "headline").first if let headlineBlock { print("Found block named:", try engine.block.getName(headlineBlock)) } ``` ### Discover All Placeholders Use `findAllPlaceholders()` to discover every placeholder block in a template and iterate through them programmatically. ```swift highlight-replaceContent-findAllPlaceholders // Discover every placeholder block so you can iterate over them. let placeholders = engine.block.findAllPlaceholders() print("Template placeholders:", placeholders.count) ``` ### Query Placeholder State Graphic blocks keep their replaceable content in a fill, so placeholder behavior is a property of the fill — get it with `getFill(_:)` and pass it to `supportsPlaceholderBehavior(_:)`. The interactive placeholder flag stays on the block, so read it from the block with `isPlaceholderEnabled(_:)`. (Text blocks have no replaceable fill, so their behavior is queried on the block directly.) ```swift highlight-replaceContent-queryState if let imageBlock = engine.block.find(byName: "product-image").first { let fill = try engine.block.getFill(imageBlock) if try engine.block.supportsPlaceholderBehavior(fill) { let enabled = try engine.block.isPlaceholderEnabled(imageBlock) print("product-image placeholder enabled:", enabled) } } ``` ## Text Variable Replacement Replace text content through CE.SDK's variable system. A text block containing `{{variableName}}` updates automatically when you set the matching variable with `set(key:value:)`. ```swift highlight-replaceContent-textVariables // The headline block contains "{{headline}}" and updates when the variable is set. try engine.variable.set(key: "headline", value: "Summer Sale") ``` ### Managing Variables List every variable the template references with `findAll()`, read a current value with `get(key:)`, and delete one you no longer need with `remove(key:)`. ```swift highlight-replaceContent-manageVariables // List every variable the template references and read a current value. let variableNames = engine.variable.findAll() let headlineValue = try engine.variable.get(key: "headline") print("Variables:", variableNames, "headline =", headlineValue) // Remove a variable you no longer need. try engine.variable.set(key: "legacyTag", value: "obsolete") try engine.variable.remove(key: "legacyTag") ``` ## Replacing Image Content Update an image placeholder by modifying the fill's image source. Get the fill block with `getFill(_:)`, then set the new URL on the `fill/image/imageFileURI` property with `setURL(_:property:value:)`. ```swift highlight-replaceContent-replaceImage // Swap an image placeholder's source by updating its fill's image URI. if let imageBlock = engine.block.find(byName: "product-image").first { let fill = try engine.block.getFill(imageBlock) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"), ) } ``` ## Direct Text Replacement Replace the full text of a block without the variable system using `replaceText(_:text:in:)`. This is the right tool when you need precise control over the exact string a block displays. ```swift highlight-replaceContent-directText // Replace the full text of a block without using the variable system. if let subtitleBlock = engine.block.find(byName: "subtitle").first { try engine.block.replaceText(subtitleBlock, text: "Up to 50% off this week") } ``` ## Data-Driven Template Workflows Build automated template population by iterating over data records. Update the variables and placeholders for each record, then export the page before moving on to the next one. ```swift highlight-replaceContent-dataDriven // Populate the template once per record, then export each result. let records: [[String: String]] = [ ["headline": "Summer Sale", "subtitle": "Up to 50% off", "image": "ly.img.image/images/sample_1.jpg"], ["headline": "Winter Sale", "subtitle": "Cozy deals inside", "image": "ly.img.image/images/sample_2.jpg"], ] for record in records { if let headline = record["headline"] { try engine.variable.set(key: "headline", value: headline) } if let subtitle = record["subtitle"], let subtitleBlock = engine.block.find(byName: "subtitle").first { try engine.block.replaceText(subtitleBlock, text: subtitle) } if let imagePath = record["image"], let imageBlock = engine.block.find(byName: "product-image").first { let fill = try engine.block.getFill(imageBlock) try engine.block.setURL( fill, property: "fill/image/imageFileURI", value: baseURL.appendingPathComponent(imagePath), ) } let blob = try await engine.block.export(page, mimeType: .png) print("Exported \(record["headline"] ?? "record"):", blob.count, "bytes") } ``` ## Troubleshooting ### Block Not Found by Name Verify the exact name string matches what's set in the template. Names are case-sensitive. Use `getName(_:)` to inspect existing block names. ### Variable Not Replacing Text Ensure the `{{variableName}}` token in the text block matches the key passed to `set(key:value:)` exactly, including casing. ### Image Not Updating Confirm the block has an image fill by checking that `getFill(_:)` returns a valid fill block. Verify the URL is reachable and properly formatted. ### Placeholder State Queries Return False For a graphic block, query `supportsPlaceholderBehavior(_:)` on its fill (from `getFill(_:)`), not on the block — placeholder behavior lives on the fill, so checking the graphic block returns `false`. Text blocks are queried on the block directly. ## API Reference | Method | Description | |--------|-------------| | `block.find(byName:)` | Find blocks by name identifier | | `block.getName(_:)` | Get the name of a block | | `block.findAllPlaceholders()` | Discover all placeholder blocks in the scene | | `block.isPlaceholderEnabled(_:)` | Check whether placeholder functionality is enabled | | `block.supportsPlaceholderBehavior(_:)` | Verify a fill (graphic blocks) or block (text blocks) supports placeholder behavior | | `block.getFill(_:)` | Get the fill block from a graphic block | | `block.setURL(_:property:value:)` | Set URL-valued properties such as image sources | | `block.replaceText(_:text:in:)` | Replace text content directly | | `block.export(_:mimeType:)` | Export a block to an image format | | `variable.set(key:value:)` | Set a text variable value for dynamic replacement | | `variable.get(key:)` | Get the current value of a variable | | `variable.findAll()` | List all variable names in the scene | | `variable.remove(key:)` | Remove a variable from the scene | ## Next Steps [Data Merge](https://img.ly/docs/cesdk/mac-catalyst/automation/data-merge-ae087c/) — Automate filling a template from structured data records. [Product Variations](https://img.ly/docs/cesdk/mac-catalyst/automation/product-variations-f3349f/) — Generate multiple design variations from a single template. [Templating](https://img.ly/docs/cesdk/mac-catalyst/concepts/templating-f94385/) — Learn the template model behind variables and placeholders. [Placeholders](https://img.ly/docs/cesdk/mac-catalyst/create-templates/add-dynamic-content/placeholders-d9ba8a/) — Configure placeholder behavior and controls 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: "User Interface" description: "Use CE.SDK’s customizable, production-ready UI or replace it entirely with your own interface." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/user-interface-5a089a/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/mac-catalyst/user-interface-5a089a/) --- --- ## Related Pages - [Customization](https://img.ly/docs/cesdk/mac-catalyst/user-interface/customization-72b2f8/) - Control which features are available and how UI components behave, appear, or are arranged in the editor. - [Build Your Own UI](https://img.ly/docs/cesdk/mac-catalyst/user-interface/build-your-own-ui-fe7527/) - Build a completely custom editor UI using CE.SDK's headless Swift engine APIs and flexible integration 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: "Build Your Own UI" description: "Build a completely custom editor UI using CE.SDK's headless Swift engine APIs and flexible integration options." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/user-interface/build-your-own-ui-fe7527/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/mac-catalyst/user-interface-5a089a/) > [Build Your Own UI](https://img.ly/docs/cesdk/mac-catalyst/user-interface/build-your-own-ui-fe7527/) --- Build completely custom UIs by driving CE.SDK's Swift engine directly from SwiftUI, UIKit, or AppKit — host the canvas, own the controls, and stay on the engine APIs end to end. > **Reading time:** 12 minutes > > **Resources:** > > - [View source on GitHub](https://github.com/imgly/cesdk-swift-examples/tree/v$UBQ_VERSION$/engine-guides-user-interface-build-your-own-ui) When you need complete control over the editing experience, the headless engine lets you build entirely custom interfaces. You initialize the engine directly and pair it with your own controls, panels, and workflows that match your application's design system. ```swift file=@cesdk_swift_examples/engine-guides-user-interface-build-your-own-ui/BuildYourOwnUI.swift reference-only import Foundation import IMGLYEngine import SwiftUI // MARK: - View-model — drives the demo UI and every highlight block @MainActor final class BuildYourOwnUIViewModel: ObservableObject { @Published private(set) var selectedBlockID: DesignBlockID? @Published private(set) var selectedType: String? @Published var positionX: Float = 0 @Published var positionY: Float = 0 @Published var width: Float = 0 @Published var height: Float = 0 @Published var rotationDegrees: Float = 0 let engine: Engine private var pageID: DesignBlockID? private var eventTask: Task? init(engine: Engine) { self.engine = engine } deinit { eventTask?.cancel() } func setupScene() async { do { 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) pageID = page try createInitialContent(on: page) try await engine.scene.zoom( to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) startEventLoop() } catch { print("Scene setup failed: \(error)") } } private func createInitialContent(on page: DesignBlockID) throws { let textBlock = try engine.block.create(.text) try engine.block.setString(textBlock, property: "text/text", value: "Click to Edit") try engine.block.setPositionX(textBlock, value: 80) try engine.block.setPositionY(textBlock, value: 80) try engine.block.setWidth(textBlock, value: 300) try engine.block.setHeight(textBlock, value: 80) try engine.block.appendChild(to: page, child: textBlock) let shapeBlock = try engine.block.create(.graphic) try engine.block.setShape(shapeBlock, 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.2, g: 0.6, b: 0.9, a: 1)) try engine.block.setFill(shapeBlock, fill: fill) try engine.block.setPositionX(shapeBlock, value: 450) try engine.block.setPositionY(shapeBlock, value: 200) try engine.block.setWidth(shapeBlock, value: 150) try engine.block.setHeight(shapeBlock, value: 150) try engine.block.appendChild(to: page, child: shapeBlock) try engine.block.select(textBlock) } private func startEventLoop() { // Capture `engine` and reference `self` weakly: a strong `self` held across // the `for await` suspension would retain the view-model and its engine for // the lifetime of the subscription. eventTask = Task { [weak self, engine] in for await events in engine.event.subscribe(to: []) { self?.refreshSelection(from: events) } } } private func refreshSelection(from _: [BlockEvent]) { let selected = engine.block.findAllSelected().first selectedBlockID = selected guard let selected, engine.block.isValid(selected) else { selectedType = nil return } do { selectedType = try engine.block.getType(selected) positionX = try engine.block.getPositionX(selected) positionY = try engine.block.getPositionY(selected) width = try engine.block.getWidth(selected) height = try engine.block.getHeight(selected) rotationDegrees = try engine.block.getRotation(selected) * 180 / .pi } catch { selectedType = nil } } func addText() { guard let pageID else { return } do { let textBlock = try engine.block.create(.text) try engine.block.setString(textBlock, property: "text/text", value: "Lorem ipsum dolor sit amet") try engine.block.setPositionX(textBlock, value: 80) try engine.block.setPositionY(textBlock, value: 80) try engine.block.setWidth(textBlock, value: 300) try engine.block.setHeight(textBlock, value: 100) try engine.block.appendChild(to: pageID, child: textBlock) try engine.block.select(textBlock) } catch { print("Add text failed: \(error)") } } func addShape() { guard let pageID else { return } do { let shapeBlock = try engine.block.create(.graphic) try engine.block.setShape(shapeBlock, 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.2, g: 0.6, b: 0.9, a: 1)) try engine.block.setFill(shapeBlock, fill: fill) try engine.block.setPositionX(shapeBlock, value: 80) try engine.block.setPositionY(shapeBlock, value: 80) try engine.block.setWidth(shapeBlock, value: 150) try engine.block.setHeight(shapeBlock, value: 150) try engine.block.appendChild(to: pageID, child: shapeBlock) try engine.block.select(shapeBlock) } catch { print("Add shape failed: \(error)") } } func setPositionX(_ value: Float) { guard let id = selectedBlockID else { return } try? engine.block.setPositionX(id, value: value) } func setPositionY(_ value: Float) { guard let id = selectedBlockID else { return } try? engine.block.setPositionY(id, value: value) } func setWidth(_ value: Float) { guard let id = selectedBlockID else { return } try? engine.block.setWidth(id, value: value) } func setHeight(_ value: Float) { guard let id = selectedBlockID else { return } try? engine.block.setHeight(id, value: value) } func setRotationDegrees(_ value: Float) { guard let id = selectedBlockID else { return } try? engine.block.setRotation(id, radians: value * .pi / 180) } func export() async -> Data? { guard let pageID else { return nil } return try? await engine.block.export(pageID, mimeType: .png) } } // MARK: - The view — Canvas on top, controls below struct BuildYourOwnUIView: View { @StateObject private var viewModel: BuildYourOwnUIViewModel init(engine: Engine) { _viewModel = StateObject(wrappedValue: BuildYourOwnUIViewModel(engine: engine)) } var body: some View { VStack(spacing: 0) { Canvas(engine: viewModel.engine) .frame(maxWidth: .infinity, maxHeight: .infinity) Divider() controls .padding() .background(Color(white: 0.95)) } .onAppear { Task { await viewModel.setupScene() } } } private var controls: some View { VStack(alignment: .leading, spacing: 12) { HStack { Button("Add Text") { viewModel.addText() } Button("Add Shape") { viewModel.addShape() } Spacer() Button("Export PNG") { Task { _ = await viewModel.export() } } } if viewModel.selectedBlockID != nil { Text("Selected: \(viewModel.selectedType ?? "—")") .font(.caption) .foregroundColor(.secondary) propertyRow("X", value: $viewModel.positionX, in: 0 ... 800, onChange: viewModel.setPositionX) propertyRow("Y", value: $viewModel.positionY, in: 0 ... 600, onChange: viewModel.setPositionY) propertyRow("W", value: $viewModel.width, in: 1 ... 800, onChange: viewModel.setWidth) propertyRow("H", value: $viewModel.height, in: 1 ... 600, onChange: viewModel.setHeight) propertyRow( "Rot°", value: $viewModel.rotationDegrees, in: -180 ... 180, onChange: viewModel.setRotationDegrees, ) } else { Text("Tap a block on the canvas to edit its properties.") .font(.caption) .foregroundColor(.secondary) } } } private func propertyRow( _ label: String, value: Binding, in range: ClosedRange, onChange: @escaping (Float) -> Void, ) -> some View { HStack { Text(label).font(.caption).frame(width: 36, alignment: .leading) Slider(value: value, in: range) { editing in if !editing { onChange(value.wrappedValue) } } Text("\(Int(value.wrappedValue))") .font(.caption.monospacedDigit()) .frame(width: 44, alignment: .trailing) } } } // MARK: - Minimal Canvas host used in "Initialize Engine and Setup Canvas" struct MinimalCanvasView: View { @State private var engine: Engine? var body: some View { Group { if let engine { Canvas(engine: engine) } else { ProgressView("Initializing engine…") } } .onAppear { Task { engine = try? await Engine( license: secrets.licenseKey, // pass nil for evaluation mode with watermark userID: "", ) } } } } // MARK: - UIKit and AppKit hosts — for apps that own their own MTKView #if canImport(UIKit) && !os(watchOS) import MetalKit import UIKit final class BuildYourOwnUIController: UIViewController { private var engine: Engine? private lazy var canvas = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice()) override func viewDidLoad() { super.viewDidLoad() view.addSubview(canvas) canvas.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ canvas.leftAnchor.constraint(equalTo: view.leftAnchor), canvas.rightAnchor.constraint(equalTo: view.rightAnchor), canvas.topAnchor.constraint(equalTo: view.topAnchor), canvas.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) Task { engine = try await Engine( context: .metalView(view: canvas), license: secrets.licenseKey, // pass nil for evaluation mode with watermark userID: "", ) engine?.onAppear() } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) engine?.onDisappear() } } #endif #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit import MetalKit final class BuildYourOwnUIControllerMac: NSViewController { private var engine: Engine? private lazy var canvas = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice()) override func loadView() { view = NSView(frame: .init(x: 0, y: 0, width: 1000, height: 700)) } override func viewDidLoad() { super.viewDidLoad() view.addSubview(canvas) canvas.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ canvas.leftAnchor.constraint(equalTo: view.leftAnchor), canvas.rightAnchor.constraint(equalTo: view.rightAnchor), canvas.topAnchor.constraint(equalTo: view.topAnchor), canvas.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } override func viewDidAppear() { super.viewDidAppear() Task { engine = try await Engine( context: .metalView(view: canvas), license: secrets.licenseKey, // pass nil for evaluation mode with watermark userID: "", ) engine?.onAppear() } } override func viewWillDisappear() { super.viewWillDisappear() engine?.onDisappear() } } #endif // MARK: - #Preview — boot a real engine in evaluation mode #if DEBUG /// Live preview that boots a real engine in evaluation mode so the file /// can be exercised inside Xcode without launching a host app. /// Requires Xcode 15+ for the `#Preview` macro. @available(iOS 17, macOS 14, *) #Preview { BuildYourOwnUIPreview() } /// `#Preview` bodies cannot host async initialization directly on the /// iOS 14 / macOS 11 deployment targets the package supports, so the /// preview body uses `.onAppear` instead of `.task`. private struct BuildYourOwnUIPreview: View { @State private var engine: Engine? var body: some View { Group { if let engine { BuildYourOwnUIView(engine: engine) } else { ProgressView("Booting engine…") } } .onAppear { Task { engine = try? await Engine( license: nil, // evaluation mode with watermark — fine for previews userID: "", ) } } } } #endif // MARK: - Test-runnable entry point /// Drives every code path that `BuildYourOwnUIView` exercises against a /// shared offscreen test engine — `setupScene`, the event loop, intent /// methods on the view-model, and the export call. Verifies that the /// rendered guide's highlights stay runtime-correct without spinning up /// a SwiftUI view. @MainActor func buildYourOwnUI(engine: Engine) async throws { let viewModel = BuildYourOwnUIViewModel(engine: engine) await viewModel.setupScene() viewModel.addText() viewModel.addShape() viewModel.setPositionX(120) viewModel.setPositionY(140) viewModel.setRotationDegrees(15) let png = await viewModel.export() print("Exported \(png?.count ?? 0) bytes") } ``` This guide covers initializing the engine, hosting its canvas in SwiftUI, subscribing to events, building interactive controls, and exporting designs — all while keeping the user experience yours. ## Architecture Overview The headless engine separates rendering from UI. Your code calls methods like `engine.block.create(.text)` and `engine.block.setPositionX(_:value:)` to manipulate the design; the engine notifies you of changes through `engine.event.subscribe(to:)`. The view-model in the bundled example demonstrates this shape — it owns the engine, subscribes to events, and republishes block state as `@Published` properties for SwiftUI to observe. ```swift highlight-buildYourOwnUI-viewModel @MainActor final class BuildYourOwnUIViewModel: ObservableObject { @Published private(set) var selectedBlockID: DesignBlockID? @Published private(set) var selectedType: String? @Published var positionX: Float = 0 @Published var positionY: Float = 0 @Published var width: Float = 0 @Published var height: Float = 0 @Published var rotationDegrees: Float = 0 let engine: Engine private var pageID: DesignBlockID? private var eventTask: Task? init(engine: Engine) { self.engine = engine } deinit { eventTask?.cancel() } ``` `Engine` is `@MainActor`-isolated, so the compiler enforces that all engine calls run on the main thread. ## Initialize Engine and Setup Canvas `Engine(context:audioContext:license:userID:)` constructs an engine. Pick the context that matches how you want to render: | Context | When to use | | --- | --- | | `.metal` | Default for SwiftUI. The engine creates its own Metal view; `IMGLYEngine.Canvas` hosts it inside your view hierarchy. | | `.metalView(view:)` | Bind the engine to an `MTKView` your view controller already owns. Covered under [Framework Integration Patterns](https://img.ly/docs/cesdk/mac-catalyst/user-interface/build-your-own-ui-fe7527/#framework-integration-patterns). | | `.offscreen(size:)` | Headless rendering for export-only workflows with no visible canvas. | For a custom SwiftUI editor, `.metal` plus `IMGLYEngine.Canvas(engine:)` is the canonical shape — and since `.metal` is the default for `context:`, the parameter can be omitted entirely. `Canvas` adopts the engine's Metal view into your hierarchy and forwards `onAppear` / `onDisappear` to the engine. The hooks are no-ops today but are part of the public engine API; calling them keeps your integration future-proof if the engine starts using them. ```swift highlight-buildYourOwnUI-canvasView struct MinimalCanvasView: View { @State private var engine: Engine? var body: some View { Group { if let engine { Canvas(engine: engine) } else { ProgressView("Initializing engine…") } } .onAppear { Task { engine = try? await Engine( license: secrets.licenseKey, // pass nil for evaluation mode with watermark userID: "", ) } } } } ``` The minimal host above is the smallest viable shape. Wrap or compose `MinimalCanvasView` with your own toolbars, inspectors, and sidebars to build the surrounding UI. The remainder of this guide walks through that pairing. ## Create Initial Content Seed the scene with a page that defines the workspace dimensions, then add a text block and a shape so the user has something to interact with on launch. The same APIs (`engine.scene.create`, `engine.block.create`, the setters that follow) are used for both initial content and content the user adds later. ```swift highlight-buildYourOwnUI-setup func setupScene() async { do { 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) pageID = page try createInitialContent(on: page) try await engine.scene.zoom( to: page, paddingLeft: 40, paddingTop: 40, paddingRight: 40, paddingBottom: 40, ) startEventLoop() } catch { print("Scene setup failed: \(error)") } } ``` ```swift highlight-buildYourOwnUI-createInitialContent private func createInitialContent(on page: DesignBlockID) throws { let textBlock = try engine.block.create(.text) try engine.block.setString(textBlock, property: "text/text", value: "Click to Edit") try engine.block.setPositionX(textBlock, value: 80) try engine.block.setPositionY(textBlock, value: 80) try engine.block.setWidth(textBlock, value: 300) try engine.block.setHeight(textBlock, value: 80) try engine.block.appendChild(to: page, child: textBlock) let shapeBlock = try engine.block.create(.graphic) try engine.block.setShape(shapeBlock, 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.2, g: 0.6, b: 0.9, a: 1)) try engine.block.setFill(shapeBlock, fill: fill) try engine.block.setPositionX(shapeBlock, value: 450) try engine.block.setPositionY(shapeBlock, value: 200) try engine.block.setWidth(shapeBlock, value: 150) try engine.block.setHeight(shapeBlock, value: 150) try engine.block.appendChild(to: page, child: shapeBlock) try engine.block.select(textBlock) } ``` Graphic blocks need both a shape (`createShape(.rect)`) and a fill (`createFill(.color)`) before they render. Text blocks render their default string immediately and are styled later via property setters. ## Handle Engine Events Subscribe to block lifecycle events to keep your UI synchronized with the engine. The event API delivers `Created`, `Updated`, and `Destroyed` notifications, batched at the end of each engine update cycle. Pass `[]` to receive every event or an array of `DesignBlockID`s to scope the subscription. ```swift highlight-buildYourOwnUI-handleEvents private func startEventLoop() { // Capture `engine` and reference `self` weakly: a strong `self` held across // the `for await` suspension would retain the view-model and its engine for // the lifetime of the subscription. eventTask = Task { [weak self, engine] in for await events in engine.event.subscribe(to: []) { self?.refreshSelection(from: events) } } } private func refreshSelection(from _: [BlockEvent]) { let selected = engine.block.findAllSelected().first selectedBlockID = selected guard let selected, engine.block.isValid(selected) else { selectedType = nil return } do { selectedType = try engine.block.getType(selected) positionX = try engine.block.getPositionX(selected) positionY = try engine.block.getPositionY(selected) width = try engine.block.getWidth(selected) height = try engine.block.getHeight(selected) rotationDegrees = try engine.block.getRotation(selected) * 180 / .pi } catch { selectedType = nil } } ``` By republishing the selected block's properties through `@Published`, the view-model lets SwiftUI re-render the property panel on every selection change and every external mutation — the binding is bidirectional and event-driven. ## Build Custom UI Controls The view holds the view-model in `@StateObject`, hands it an engine on init, and lays the canvas above the controls in a vertical stack. `setupScene()` runs once on appear to build the scene and start the event loop — the same lifecycle pattern `MinimalCanvasView` used, scaled up with a real sidebar. ```swift highlight-buildYourOwnUI-view struct BuildYourOwnUIView: View { @StateObject private var viewModel: BuildYourOwnUIViewModel init(engine: Engine) { _viewModel = StateObject(wrappedValue: BuildYourOwnUIViewModel(engine: engine)) } var body: some View { VStack(spacing: 0) { Canvas(engine: viewModel.engine) .frame(maxWidth: .infinity, maxHeight: .infinity) Divider() controls .padding() .background(Color(white: 0.95)) } .onAppear { Task { await viewModel.setupScene() } } } ``` The sidebar itself is the toolbar and property inspector. Each control calls back into the view-model's intent methods; the view-model rewrites the engine, the engine fires an event, the event loop refreshes the published state, and the controls pick up the new values. The whole flow is one closed loop and works the same way for a `Button`, a `Slider`, or any other SwiftUI input. ```swift highlight-buildYourOwnUI-uiControls private var controls: some View { VStack(alignment: .leading, spacing: 12) { HStack { Button("Add Text") { viewModel.addText() } Button("Add Shape") { viewModel.addShape() } Spacer() Button("Export PNG") { Task { _ = await viewModel.export() } } } if viewModel.selectedBlockID != nil { Text("Selected: \(viewModel.selectedType ?? "—")") .font(.caption) .foregroundColor(.secondary) propertyRow("X", value: $viewModel.positionX, in: 0 ... 800, onChange: viewModel.setPositionX) propertyRow("Y", value: $viewModel.positionY, in: 0 ... 600, onChange: viewModel.setPositionY) propertyRow("W", value: $viewModel.width, in: 1 ... 800, onChange: viewModel.setWidth) propertyRow("H", value: $viewModel.height, in: 1 ... 600, onChange: viewModel.setHeight) propertyRow( "Rot°", value: $viewModel.rotationDegrees, in: -180 ... 180, onChange: viewModel.setRotationDegrees, ) } else { Text("Tap a block on the canvas to edit its properties.") .font(.caption) .foregroundColor(.secondary) } } } private func propertyRow( _ label: String, value: Binding, in range: ClosedRange, onChange: @escaping (Float) -> Void, ) -> some View { HStack { Text(label).font(.caption).frame(width: 36, alignment: .leading) Slider(value: value, in: range) { editing in if !editing { onChange(value.wrappedValue) } } Text("\(Int(value.wrappedValue))") .font(.caption.monospacedDigit()) .frame(width: 44, alignment: .trailing) } } ``` The toolbar lives below the canvas in this example so portrait iPhones still get a usably-sized canvas. On macOS and Mac Catalyst the same vertical layout reads naturally; you can also adapt the layout per size class with `HStack` on regular widths. ## Add Blocks Programmatically Add new content by calling `engine.block.create(_:)` with the block type you want, configuring its properties, and appending it to the page hierarchy. The example demonstrates text and graphic blocks; other block types (`.audio`, `.video`) follow the same create-configure-append shape but require additional setup such as URL loading and AV resource preparation. ```swift highlight-buildYourOwnUI-addBlocks func addText() { guard let pageID else { return } do { let textBlock = try engine.block.create(.text) try engine.block.setString(textBlock, property: "text/text", value: "Lorem ipsum dolor sit amet") try engine.block.setPositionX(textBlock, value: 80) try engine.block.setPositionY(textBlock, value: 80) try engine.block.setWidth(textBlock, value: 300) try engine.block.setHeight(textBlock, value: 100) try engine.block.appendChild(to: pageID, child: textBlock) try engine.block.select(textBlock) } catch { print("Add text failed: \(error)") } } func addShape() { guard let pageID else { return } do { let shapeBlock = try engine.block.create(.graphic) try engine.block.setShape(shapeBlock, 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.2, g: 0.6, b: 0.9, a: 1)) try engine.block.setFill(shapeBlock, fill: fill) try engine.block.setPositionX(shapeBlock, value: 80) try engine.block.setPositionY(shapeBlock, value: 80) try engine.block.setWidth(shapeBlock, value: 150) try engine.block.setHeight(shapeBlock, value: 150) try engine.block.appendChild(to: pageID, child: shapeBlock) try engine.block.select(shapeBlock) } catch { print("Add shape failed: \(error)") } } ``` Each intent method demonstrates the recipe: create the block, configure its properties, append it to the page, and select it for immediate editing. The toolbar's "Add Text" and "Add Shape" buttons hand the user direct access to these flows. ## Create Property Panels The property panel binds the selected block's position, size, and rotation to real `Slider` controls. The view-model's `@Published` properties hold the current values; the `Slider`'s `onEditingChanged` callback writes them back to the engine. ```swift highlight-buildYourOwnUI-propertyPanel func setPositionX(_ value: Float) { guard let id = selectedBlockID else { return } try? engine.block.setPositionX(id, value: value) } func setPositionY(_ value: Float) { guard let id = selectedBlockID else { return } try? engine.block.setPositionY(id, value: value) } func setWidth(_ value: Float) { guard let id = selectedBlockID else { return } try? engine.block.setWidth(id, value: value) } func setHeight(_ value: Float) { guard let id = selectedBlockID else { return } try? engine.block.setHeight(id, value: value) } func setRotationDegrees(_ value: Float) { guard let id = selectedBlockID else { return } try? engine.block.setRotation(id, radians: value * .pi / 180) } ``` `setRotation(_:radians:)` takes radians; the view-model converts the slider's `Float` degrees to radians before calling into the engine. The view-model's `refreshSelection` does the reverse conversion when the engine reports a new rotation, so the slider always shows degrees. ## Export Designs Export a block (or the entire scene) with `engine.block.export(_:mimeType:)`. The call is async and returns raw `Data` you can write to disk, hand to a share sheet, or upload to your backend. ```swift highlight-buildYourOwnUI-export func export() async -> Data? { guard let pageID else { return nil } return try? await engine.block.export(pageID, mimeType: .png) } ``` The example exports the page as PNG. Pass a different `MIMEType` (`.jpeg`, `.webp`, `.pdf`) to export the same block in another format — the rest of the call stays the same. ## Framework Integration Patterns `IMGLYEngine.Canvas` is the default SwiftUI integration: it owns the underlying Metal view and forwards `onAppear` / `onDisappear` to the engine for you. For apps that already own their own `MTKView` — typically UIKit and AppKit hosts — initialize the engine with `Engine.Context.metalView(view:)` and the engine binds to your existing view instead of creating its own. ```swift highlight-buildYourOwnUI-uikitHost final class BuildYourOwnUIController: UIViewController { private var engine: Engine? private lazy var canvas = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice()) override func viewDidLoad() { super.viewDidLoad() view.addSubview(canvas) canvas.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ canvas.leftAnchor.constraint(equalTo: view.leftAnchor), canvas.rightAnchor.constraint(equalTo: view.rightAnchor), canvas.topAnchor.constraint(equalTo: view.topAnchor), canvas.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) Task { engine = try await Engine( context: .metalView(view: canvas), license: secrets.licenseKey, // pass nil for evaluation mode with watermark userID: "", ) engine?.onAppear() } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) engine?.onDisappear() } } ``` ```swift highlight-buildYourOwnUI-appkitHost final class BuildYourOwnUIControllerMac: NSViewController { private var engine: Engine? private lazy var canvas = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice()) override func loadView() { view = NSView(frame: .init(x: 0, y: 0, width: 1000, height: 700)) } override func viewDidLoad() { super.viewDidLoad() view.addSubview(canvas) canvas.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ canvas.leftAnchor.constraint(equalTo: view.leftAnchor), canvas.rightAnchor.constraint(equalTo: view.rightAnchor), canvas.topAnchor.constraint(equalTo: view.topAnchor), canvas.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } override func viewDidAppear() { super.viewDidAppear() Task { engine = try await Engine( context: .metalView(view: canvas), license: secrets.licenseKey, // pass nil for evaluation mode with watermark userID: "", ) engine?.onAppear() } } override func viewWillDisappear() { super.viewWillDisappear() engine?.onDisappear() } } ``` The UIKit and AppKit patterns are mirror images of each other — own an `MTKView`, hand it to the engine, call `engine.onAppear()` / `engine.onDisappear()` from `viewDidAppear` / `viewWillDisappear` to keep your integration future-proof. The hooks are no-ops in the engine today but are part of the public API and may track lifecycle events in a future release; `IMGLYEngine.Canvas` already calls them for you in SwiftUI. The engine, the event loop, and the intent methods (`addText`, `setPositionX`, `export`, …) are framework-agnostic and carry over to a `UIViewController` or `NSViewController`. `BuildYourOwnUIViewModel` exposes its reactive state through Combine — `ObservableObject` and `@Published` are Combine types that SwiftUI consumes natively via `@StateObject` — so a UIKit or AppKit host can subscribe to the same publishers via Combine's `.sink { ... }` to update `UISlider` / `NSSlider`, and call the intent methods from `valueChanged` selectors instead of SwiftUI's `onEditingChanged` closures. ## Troubleshooting ### Canvas Not Rendering **Problem:** The Metal view appears but shows no content. **Solution:** Confirm the engine was initialized with `.metal` (or `.metalView(view:)`) and that `engine.scene.create()` ran. `IMGLYEngine.Canvas` requires one of those two contexts — `.offscreen(size:)` has no view to render. ### Events Not Firing **Problem:** The property panel doesn't update when blocks change. **Solution:** Ensure the `for await events in engine.event.subscribe(to: [])` loop is running inside a long-lived `Task`. The view-model owns the task in `eventTask` and cancels it on `deinit`; if you re-create the view-model on every render the subscription gets torn down before any event fires. ### Performance Issues **Problem:** Slider drags feel sluggish. **Solution:** Write back to the engine on `onEditingChanged` (when the drag ends) instead of on every interim value, as the example does. Continuous updates fire one engine event per pixel and can saturate the main actor. ### Blocks Not Responding **Problem:** Newly created blocks don't appear. **Solution:** Confirm the block was appended to the scene hierarchy with `engine.block.appendChild(to:child:)`. Blocks created with `engine.block.create(_:)` are detached until appended. ### Selection State Out of Sync **Problem:** The property panel shows stale values. **Solution:** Use `engine.block.select(_:)` (single-select) or `engine.block.setSelected(_:selected:)` (toggle). The view-model reads selection via `engine.block.findAllSelected()` whenever an event arrives, so explicit selection changes flow through the same path as user taps on the canvas. ## API Reference | Method | Category | Purpose | | --- | --- | --- | | `Engine(context:audioContext:license:userID:)` | Engine | Initialize the engine on the main actor. | | `Engine.Context.metal` | Engine | Let the engine create its own Metal view (use with `IMGLYEngine.Canvas`). | | `Engine.Context.metalView(view:)` | Engine | Bind the engine to an `MTKView` your view controller already owns. | | `Engine.Context.offscreen(size:)` | Engine | Headless rendering for export-only workflows. | | `engine.onAppear()` / `engine.onDisappear()` | Engine | Forward-compatibility lifecycle hooks for UIKit / AppKit hosts. No-op in the engine today; `IMGLYEngine.Canvas` calls them automatically in SwiftUI. | | `IMGLYEngine.Canvas(engine:isPaused:)` | SwiftUI | A `View` that hosts the engine's Metal canvas. | | `engine.scene.create()` | Scene | Create a new blank scene. | | `engine.scene.zoom(to:paddingLeft:paddingTop:paddingRight:paddingBottom:)` | Scene | Zoom the viewport to a block. | | `engine.block.create(_:)` | Block | Create a new block of the given `DesignBlockType`. | | `engine.block.appendChild(to:child:)` | Block | Add a block to the scene hierarchy. | | `engine.block.select(_:)` / `setSelected(_:selected:)` | Block | Select a block (single / toggle). | | `engine.block.findAllSelected()` | Block | List currently selected blocks. | | `engine.block.setPositionX/Y(_:value:)` | Block | Set the block's x or y position. | | `engine.block.setWidth/Height(_:value:)` | Block | Set the block's width or height. | | `engine.block.setRotation(_:radians:)` | Block | Set the block's rotation in radians. | | `engine.block.getType(_:)` | Block | Read the block's type as a string. | | `engine.block.setString(_:property:value:)` | Block | Write a string property (e.g. `text/text`). | | `engine.block.setColor(_:property:color:)` | Block | Write a color property (e.g. `fill/color/value`). | | `engine.block.createShape(_:)` / `setShape(_:shape:)` | Block | Create and attach a shape to a graphic block. | | `engine.block.createFill(_:)` / `setFill(_:fill:)` | Block | Create and attach a fill to a graphic block. | | `engine.block.export(_:mimeType:)` | Block | Export a block (or scene) to `Data`. | | `engine.event.subscribe(to:)` | Event | `AsyncStream` of block lifecycle events. | ## Next Steps - [Engine Interface](https://img.ly/docs/cesdk/mac-catalyst/engine-interface-6fb7cf/) — Deep dive into headless engine concepts - [Blocks](https://img.ly/docs/cesdk/mac-catalyst/concepts/blocks-90241e/) — Understanding block types and hierarchy - [Export Designs](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: "Customization" description: "Control which features are available and how UI components behave, appear, or are arranged in the editor." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/user-interface/customization-72b2f8/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/mac-catalyst/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/mac-catalyst/user-interface/customization-72b2f8/) --- --- ## Related Pages - [Movement Constraints](https://img.ly/docs/cesdk/mac-catalyst/user-interface/customization/movement-constraints-8f3a2c/) - Restrict how far blocks can be dragged outside the page in the CE.SDK iOS editor. --- ## 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: "Movement Constraints" description: "Restrict how far blocks can be dragged outside the page in the CE.SDK iOS editor." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/user-interface/customization/movement-constraints-8f3a2c/" --- > 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/) > [User Interface](https://img.ly/docs/cesdk/mac-catalyst/user-interface-5a089a/) > [Customization](https://img.ly/docs/cesdk/mac-catalyst/user-interface/customization-72b2f8/) > [Movement Constraints](https://img.ly/docs/cesdk/mac-catalyst/user-interface/customization/movement-constraints-8f3a2c/) --- ```swift file=@cesdk_swift_examples/engine-guides-movement-constraints/MovementConstraints.swift reference-only import Foundation import IMGLYEngine @MainActor func movementConstraints(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.appendChild(to: page, child: block) // Allow every block in the scene to overshoot by 20% of its own size. try engine.editor.setMovementConstraint(MovementConstraintRule(overshoot: 0.2)) // Pin all text and caption blocks fully inside the page. try engine.editor.setMovementConstraint([ MovementConstraintRule(overshoot: 0, scope: .blockType("text")), MovementConstraintRule(overshoot: 0, scope: .blockType("caption")), ]) // Override the scene-wide default for blocks on this page. try engine.editor.setMovementConstraint( MovementConstraintRule(overshoot: 0.1, scope: .block(page)), ) // Override every other level for one specific block. try engine.editor.setMovementConstraint( MovementConstraintRule(overshoot: 0, scope: .block(block)), ) // Read the resolved constraint, walking the priority chain: // block > parent page > blockType > scene-wide. let active = try engine.editor.getMovementConstraint(block) // Clear a scope by passing the matching descriptor. Use no argument to remove // the scene-wide default. try engine.editor.removeMovementConstraint(.block(block)) // per-block try engine.editor.removeMovementConstraint(.blockType("text")) // per-type try engine.editor.removeMovementConstraint(.block(page)) // per-page try engine.editor.removeMovementConstraint() // scene-wide default _ = active } ``` Limit how far a block may extend past its page during user interactions. The constraints apply to mouse and touch gestures — moving, resizing, and scaling. API calls bypass them. `overshoot` is a non-negative fraction of the **block's own size**: `0` pins the block fully inside, `0.2` allows a 20% overshoot. Each rule's scope decides which blocks it applies to: - `.scene` — scene-wide default. - `.block(id)` — a specific block (pages count as blocks). - `.blockType(name)` — every block of the given type. ## Scene-wide default Apply a rule that affects every page in the scene: ```swift highlight-movement-constraint-scene-wide // Allow every block in the scene to overshoot by 20% of its own size. try engine.editor.setMovementConstraint(MovementConstraintRule(overshoot: 0.2)) ``` ## Per block type Scope a rule with `.blockType` to restrict all blocks of that type. Call `setMovementConstraint` with an array to apply several rules in one call: ```swift highlight-movement-constraint-per-type // Pin all text and caption blocks fully inside the page. try engine.editor.setMovementConstraint([ MovementConstraintRule(overshoot: 0, scope: .blockType("text")), MovementConstraintRule(overshoot: 0, scope: .blockType("caption")), ]) ``` ## Per page Pages are blocks, so you can target a page block to set a default for its children: ```swift highlight-movement-constraint-per-page // Override the scene-wide default for blocks on this page. try engine.editor.setMovementConstraint( MovementConstraintRule(overshoot: 0.1, scope: .block(page)), ) ``` ## Per block Target a specific block ID to override every other level: ```swift highlight-movement-constraint-per-block // Override every other level for one specific block. try engine.editor.setMovementConstraint( MovementConstraintRule(overshoot: 0, scope: .block(block)), ) ``` ## Read the active value Read the resolved constraint for a block. The lookup walks the priority chain: block, parent page, blockType, then scene-wide. It returns `nil` when the block is unconstrained. ```swift highlight-movement-constraint-read // Read the resolved constraint, walking the priority chain: // block > parent page > blockType > scene-wide. let active = try engine.editor.getMovementConstraint(block) ``` ## Remove a constraint Pass the matching `MovementConstraintScope` to clear any level of the priority chain, or call `removeMovementConstraint()` with no argument to clear the scene-wide default: ```swift highlight-movement-constraint-remove // Clear a scope by passing the matching descriptor. Use no argument to remove // the scene-wide default. try engine.editor.removeMovementConstraint(.block(block)) // per-block try engine.editor.removeMovementConstraint(.blockType("text")) // per-type try engine.editor.removeMovementConstraint(.block(page)) // per-page try engine.editor.removeMovementConstraint() // scene-wide default ``` ## Full code Here's the full code: ```swift highlight-movement-constraints import Foundation import IMGLYEngine @MainActor func movementConstraints(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.appendChild(to: page, child: block) // Allow every block in the scene to overshoot by 20% of its own size. try engine.editor.setMovementConstraint(MovementConstraintRule(overshoot: 0.2)) // Pin all text and caption blocks fully inside the page. try engine.editor.setMovementConstraint([ MovementConstraintRule(overshoot: 0, scope: .blockType("text")), MovementConstraintRule(overshoot: 0, scope: .blockType("caption")), ]) // Override the scene-wide default for blocks on this page. try engine.editor.setMovementConstraint( MovementConstraintRule(overshoot: 0.1, scope: .block(page)), ) // Override every other level for one specific block. try engine.editor.setMovementConstraint( MovementConstraintRule(overshoot: 0, scope: .block(block)), ) // Read the resolved constraint, walking the priority chain: // block > parent page > blockType > scene-wide. let active = try engine.editor.getMovementConstraint(block) // Clear a scope by passing the matching descriptor. Use no argument to remove // the scene-wide default. try engine.editor.removeMovementConstraint(.block(block)) // per-block try engine.editor.removeMovementConstraint(.blockType("text")) // per-type try engine.editor.removeMovementConstraint(.block(page)) // per-page try engine.editor.removeMovementConstraint() // scene-wide default _ = active } ``` --- ## 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: "Mac Catalyst Creative Editor" description: "Learn what CE.SDK is, how it works, and what you can build with its UI, headless API, and real-time design engine." platform: mac-catalyst url: "https://img.ly/docs/cesdk/mac-catalyst/what-is-cesdk-2e7acd/" --- > 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/) > [What is CE.SDK?](https://img.ly/docs/cesdk/mac-catalyst/what-is-cesdk-2e7acd/) --- ### What is CE.SDK? **CreativeEditor SDK (CE.SDK)** is a powerful design engine that brings fully customizable image, video, and design editing directly into your Catalyst app. Whether you're enabling AI-powered design workflows, template-based creation, dynamic content generation, or full-featured creative editing, CE.SDK offers the flexibility, performance, and developer control you need — all with minimal integration overhead. [Get Started](https://img.ly/docs/cesdk/mac-catalyst/get-started/overview-e18f40/) Trusted by leading organizations worldwide, CE.SDK powers the creative editors used in best-in-class applications, including those from Shopify, Semrush, HP, Shutterfly, Ticketmaster, and Swiss Post. ## Key Capabilities of the Mac Catalyst Creative Editor SDK ## File Format Support CE.SDK supports a wide range of file types to ensure maximum flexibility for developers: ### Importing Media ### Exporting Media ### Importing Templates For detailed information, see the [full file format support list](https://img.ly/docs/cesdk/mac-catalyst/file-format-support-3c4b2a/). ## Integrations CE.SDK supports out-of-the-box integrations with: - **Getty Images** - **Unsplash** - **Pexels** - **Soundstripe** Want to connect your own asset sources? Register a custom provider using our API. --- ## 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