Validate a design before export by detecting elements outside the page, protruding content, obscured text, and unfilled placeholders.
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.
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.
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.@MainActorprivate 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).
@MainActorprivate 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.
@MainActorprivate 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.
@MainActorprivate 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.
@MainActorprivate 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.
@MainActorprivate 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}
@MainActorprivate 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.
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 — Learn about the available export formats and options
- Blocks — Understand the block hierarchy and positioning model