Search Docs
Loading...
Skip to content

Pre-Export Validation

Validate a design before export by detecting elements outside the page, protruding content, obscured text, and unfilled placeholders.

8 mins
estimated time
GitHub

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.
@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).

@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.

@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.

@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.

@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.

@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.

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#

MethodPurpose
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