Skip to content

Compatibility report

Every conversion (and every diagnose) emits one JSON report answering: what was in this workbook, what did Rendlio do with each thing, and what should the user check? It enforces the engine-wide rule: nothing is ever silently approximated — every deviation from faithful rendering is a report entry with a location.

The report is a public, versioned API. You get it with --report <path> (or --report - for a stderr summary only); programmatic hosts should always set it.

  • Canonical machine schema (JSON Schema 2020-12): https://rendlio.com/schemas/compat-report/v1.json — the single source of truth; this page is written from it.
  • schemaVersion is semver-like "1.x": additive optional fields bump the minor version; anything else bumps the major version (and gets a new schema URL). v1.0 freezes at the first milestone release.
  • The envelope is strict: additionalProperties: false at every level, so a report never carries undocumented fields.

All top-level fields are required unless marked ?.

FieldTypeMeaning
schemaVersionstring, 1.xReport schema version
engineobjectname (always "Rendlio"), version, releaseDate (ISO date of the build)
inputobjectsha256 (64 hex chars of the input bytes), sizeBytes, fileName? (omitted for stdin)
optionsobjectCanonicalized effective options — an exact echo of the CLI flag values including defaults, plus licenseMode ("licensed:<tier>" or "evaluation"). No other licence data ever appears in a report
mode"convert" | "diagnose"Which mode produced the report
resultobjectstatus (see derivation) and exitCode (0–5)
pagesobjectcount — rendered page count; 0 for diagnose and for failures
sheetsarrayOne entry per sheet: name, index, printed (false = hidden or outside the print area)
featuresarrayThe heart of the report — see feature entries
fontsobjectsubstitutions (each requestedused with a reason: metric-substitute | fallback | missing-default) and missing (requested families with no substitute rule)
formulasobjectcellsTotal, cachedMissing (formula cells with no cached value), recalcMode (off | strict-v1), closureSize (integer when strict-v1 evaluated/refused, else null)
warningsarrayCoded warnings — see warnings
timingsobjectparseMs, layoutMs, renderMs, totalMs

Each entry in features[]:

FieldTypeMeaning
idstringStable feature identifier, pattern F-<AREA>-<slug> where <AREA> is one of PARSE, STY, NUM, LAY, CF, CHT, IMG, TXT, PDF, PNG, REC, LIC, SEC
statusenumrendered | approximated | unsupported | skipped — see below
countinteger ≥ 1Occurrences of this (feature, status) pair
locations?array, ≤ 20Where — see locations
locationsTruncated?booleantrue when more than 20 locations existed
detail?stringPlain-language statement of the rule that applied
StatusMeaningExample
renderedFaithful per the specification rule for this featuremerged cell drawn as specified
approximatedRendered, but a documented deviation applies — the detail says whichtimePeriod conditional format evaluated at conversion date
unsupportedPresent in the file, not rendered; the space it would occupy follows the specified rule for that feature (usually rendered empty)a chart type outside the supported set
skippedPresent but intentionally outside conversion semantics (not a fidelity gap)VBA project (never executed), data validation rules

Reporting rule: rendered entries are recorded only in diagnose mode — convert-mode reports list only deviations, keeping them small. approximated / unsupported / skipped always appear in both modes. So in a convert report, an empty features array means nothing deviated.

A location is { "sheet": string?, "ref": string?, "objectId": string? } with at least one field non-null. ref is an A1 cell or range ("B7", "A1:C30"); objectId is the drawing/chart part-relative id. Per feature entry, at most 20 locations are listed; beyond that, locationsTruncated is true while count keeps the real total. Output ordering is deterministic — identical runs produce byte-identical reports.

The report’s verdict and the process exit code are derived by one ladder (normative):

if internal bug → error / 5
elif resource limit hit → resource-limit / 4 (outputs deleted)
elif input invalid (not xlsx, encrypted) → invalid-input / 3
elif any feature status == unsupported → unsupported-content / 2
elif any warning OR any approximated → warnings / 1
else → success / 0

diagnose uses the same ladder but never writes documents; exit 2 there means “this file contains content Rendlio would not fully render”.

Each entry in warnings[]: code (pattern W + 4 digits), severity (info | warn | error), message (static template text — never cell contents), and an optional location. Codes are owned by engine modules in fixed ranges:

RangeDomain
W1xxxparse / OPC / ZIP
W2xxxstyles (W2001–W2099), number formats (W2100–W2199)
W25xxconditional formatting
W3xxxlayout / print
W4xxxcharts / drawings / images
W5xxxfonts / text
W6xxxformulas / recalculation
W7xxxPDF / PNG output
W8xxxlimits / security
W9xxxlicensing (e.g. W9001 — no licence found, evaluation watermark applied)

Real reports are plain JSON; comments here are annotation only.

{
"schemaVersion": "1.0",
"engine": { "name": "Rendlio", "version": "0.3.1", "releaseDate": "2026-10-02" },
"input": { "sha256": "…64 hex…", "sizeBytes": 183422, "fileName": "invoice.xlsx" },
// fileName is omitted when the input came from stdin
"options": { /* exact echo of effective CLI options, incl. defaults and licenseMode */ },
"mode": "convert", // "convert" | "diagnose"
"result": {
"status": "warnings", // the ladder's verdict …
"exitCode": 1 // … and the process exit code, always in agreement
},
"pages": { "count": 7 }, // 0 for diagnose / failures
"sheets": [
{ "name": "Summary", "index": 0, "printed": true },
{ "name": "Data", "index": 1, "printed": false } // hidden / outside print area
],
// Convert mode: deviations only. An empty array = fully faithful.
"features": [
{
"id": "F-CHT-stacked",
"status": "unsupported", // rendered | approximated | unsupported | skipped
"count": 2,
"locations": [ { "sheet": "Summary", "objectId": "chart2" } ],
"locationsTruncated": false,
"detail": "Stacked bar charts are not yet rendered (planned post-GA)."
}
],
"fonts": {
"substitutions": [
{ "requested": "Calibri", "used": "Carlito", "reason": "metric-substitute" },
{ "requested": "Aptos Narrow", "used": "Carlito", "reason": "missing-default" }
],
"missing": [ "Aptos Narrow" ]
},
"formulas": {
"cellsTotal": 1420,
"cachedMissing": 12, // formula cells with no cached value to render
"recalcMode": "off", // off | strict-v1
"closureSize": null // integer when strict-v1 evaluated/refused
},
"warnings": [
{
"code": "W5001",
"severity": "warn", // info | warn | error
"message": "Default font 'Aptos Narrow' is not available; substituted 'Carlito'. Column widths and line breaks may shift.",
"location": { "sheet": null, "ref": null, "objectId": null }
}
],
"timings": { "parseMs": 41, "layoutMs": 118, "renderMs": 96, "totalMs": 262 }
}

rendlio diagnose produces the same schema with mode: "diagnose" and pages.count: 0, and must not include cell contents: detail strings and message templates are static text plus counts and locations only. CI includes a leak test — fixtures with sentinel strings in cells assert the sentinels never appear in diagnose output. You can safely run diagnose on confidential workbooks and share the report with support.

The exit code alone is a complete gate (0 clean, 1 warnings, 2 unsupported content, 3/4/5 failures):

Terminal window
rendlio convert workbook.xlsx -o out.pdf --format pdfa --report report.json
code=$?
if [ "$code" -gt 1 ]; then
echo "conversion not clean (exit $code) — see report.json" >&2
exit "$code"
fi

Typical policy: treat <= 1 as pass and 2 as review. On exit 4 partial outputs are deleted — you never gate on a truncated document — and the report is still written, naming the limit that fired.

Fail unless the run was fully clean:

Terminal window
jq -e '.result.status == "success"' report.json

Fail on any unsupported content, but print what it was first:

Terminal window
jq -r '.features[] | select(.status == "unsupported")
| "\(.id) ×\(.count): \(.detail // "see docs")"' report.json
jq -e '[.features[] | select(.status == "unsupported")] | length == 0' report.json

Fail on font substitutions (layout may shift when fonts substitute):

Terminal window
jq -e '.fonts.substitutions | length == 0' report.json

Allow specific warning codes, fail on anything else:

Terminal window
jq -e --argjson allowed '["W9001"]' \
'[.warnings[].code] - $allowed | length == 0' report.json

Fail when formula cells are missing cached values (they render empty in off mode):

Terminal window
jq -e '.formulas.cachedMissing == 0' report.json

Validate the report itself against the published schema (for example with ajv):

Terminal window
ajv validate -s compat-report.v1.schema.json -d report.json

Because schemaVersion only gains optional fields within v1, a validator pinned to v1 keeps working across engine updates.

View as Markdown