{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/rendry-io/json-schema/rendry-io-text-fit-schema.json",
  "title": "TextFit",
  "description": "What the finished render MEASURED for one text layer. Every number is in DSL layout coordinates.",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/rendry-io-openapi.json#/components/schemas/TextFit",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "description": "The measured layer's own id. This is why text_fit is a list: the entry names its layer, the list does not"
    },
    "plate": {
      "$ref": "#/$defs/TextPlateFit",
      "description": "Present only for an assembled text plate"
    },
    "rotation_origin": {
      "$ref": "#/$defs/TextRotationOrigin",
      "description": "Present for ordinary raster text with rotation or flips; native document text uses its own placement"
    },
    "raster_top": {
      "type": "integer",
      "description": "Where the raster image put the top of the text block under vertical_align center or bottom, in whole pixels below the layer's y; absent for top alignment, no box height, a plate, and PDF or HTML output"
    },
    "size": {
      "type": "integer",
      "description": "The size that was asked for"
    },
    "fitted_size": {
      "type": "integer",
      "description": "The size auto_fit settled on; equal to size when nothing shrank"
    },
    "text_w": {
      "type": "integer",
      "description": "Width of the rendered text mask"
    },
    "text_h": {
      "type": "integer",
      "description": "Height of the rendered text mask"
    },
    "box_w": {
      "type": "integer",
      "description": "Declared layer width; 0 means unconstrained"
    },
    "box_h": {
      "type": "integer",
      "description": "Declared layer height; 0 means unconstrained"
    },
    "lines": {
      "type": "integer",
      "description": "Lines after wrapping, not only explicit newlines"
    },
    "wraps": {
      "type": "array",
      "items": {
        "type": "integer"
      },
      "description": "Where the render broke the text for width: the rune offset, in the text as drawn (variables substituted, list markers and soft hyphens in place, rich_text ** pairs removed), at which each line begins because the line before it was full. A line an explicit newline starts is not listed; absent when nothing wrapped. The editor draws its lines at these offsets"
    },
    "kept_spaces": {
      "type": "array",
      "items": {
        "type": "integer"
      },
      "description": "Visible-rune offsets of whitespace replaced with non-breaking spaces by keep_last_words. Shares the wraps coordinate system; absent when no spaces changed. The editor applies these exact substitutions before painting."
    },
    "mask_x": {
      "type": "integer",
      "description": "mask_text placement inside a shape or image"
    },
    "mask_y": {
      "type": "integer"
    },
    "mask_w": {
      "type": "integer"
    },
    "mask_h": {
      "type": "integer"
    },
    "ink_top": {
      "type": "integer",
      "description": "Mask offset from the logical text box; a diagnostic, not an alignment source — Pango bounds can include empty rows"
    },
    "ink_left": {
      "type": "integer"
    },
    "fit_group": {
      "type": "string",
      "description": "The layer's fit_group name, present only for a member of one"
    },
    "size_alone": {
      "type": "integer",
      "description": "The size this member would have taken fitted BY ITSELF, before the group levelled. Present only for a member of a fit_group; a layer in no group reports fitted_size and nothing more"
    },
    "fit_group_size": {
      "type": "integer",
      "description": "The size the group settled on — the smallest size_alone among its members. A member whose own min_size is above it keeps min_size, so fitted_size can exceed fit_group_size. Present only for a member of a fit_group"
    },
    "origin": {
      "$ref": "#/$defs/TextDiagnosticOrigin"
    },
    "list_paragraphs": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/ListParagraph"
      },
      "description": "Native list geometry on the wraps rune coordinate system; includes blank/trailing paragraphs. Absent for ordinary non-list text."
    },
    "logical_h": {
      "type": "integer",
      "minimum": 1,
      "maximum": 2097151,
      "description": "Native logical block height for lists and clip_lines, including blank lines and fallback-font metrics; absent legacy measurements keep existing line-step behavior."
    },
    "clip_end": {
      "type": "integer",
      "minimum": 0,
      "description": "Exclusive rune endpoint in the final parsed/formatted source, present only when clip_lines removed a suffix. Zero keeps a leading blank line. Slice already parsed styled content before applying wraps/list_paragraphs; this is server observation, never authored DSL."
    },
    "lines_cut": {
      "type": "integer",
      "minimum": 0,
      "description": "Number of complete native lines removed by clip_lines after full-source fitting. lines is the retained count. Absent when no lines were cut."
    }
  },
  "$defs": {
    "ListParagraph": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "rune_offset",
        "prefix_runes",
        "reserve_px",
        "continued",
        "rtl"
      ],
      "properties": {
        "rune_offset": {
          "type": "integer",
          "minimum": 0,
          "description": "Paragraph start in the same final parsed rune stream as wraps, including blank paragraphs."
        },
        "prefix_runes": {
          "type": "integer",
          "minimum": 0,
          "description": "Drawn marker/indent prefix length; zero for a midparagraph flow continuation."
        },
        "reserve_px": {
          "type": "number",
          "description": "Native physical continuation reserve in layout pixels: positive left, negative right, zero centered or unconstrained. Never infer RTL from this sign."
        },
        "continued": {
          "type": "boolean",
          "description": "Keeps an earlier paragraph marker reserve without drawing that marker again."
        },
        "rtl": {
          "type": "boolean",
          "description": "Original native resolved paragraph direction, including empty and continued paragraphs."
        }
      },
      "description": "Measured list paragraph geometry. Bind it and wraps to unchanged render inputs; ordinary non-list text omits this array. Literal list newlines normalize CRLF/CR to LF before formatting."
    },
    "TextDiagnosticOrigin": {
      "type": "object",
      "properties": {
        "key": {
          "type": "string",
          "description": "Server-created prepared structural identity; independent of generated cell IDs."
        },
        "layer_id": {
          "type": "string",
          "description": "Safe authored layer identifier; values and paths are excluded."
        },
        "table": {
          "type": "boolean"
        },
        "master": {
          "type": "boolean"
        },
        "page": {
          "type": "integer",
          "description": "Final page, 1-based."
        },
        "row": {
          "type": "integer",
          "description": "Original body row, 1-based; header and footer row zero."
        },
        "column": {
          "type": "integer",
          "description": "Column, 1-based."
        },
        "header": {
          "type": "boolean"
        },
        "master_kind": {
          "type": "string",
          "enum": [
            "first",
            "even"
          ],
          "description": "Present for replacement-master origins in diagnostics v2; keys use master_first/N or master_even/N. Default-only diagnostics remain v1."
        },
        "footer": {
          "type": "boolean",
          "description": "True for the final table footer; row is zero and header is false."
        }
      }
    },
    "TextPlateFit": {
      "type": "object",
      "description": "Actual unrotated text plate assembly in DSL pixels, before effects and output scaling. Text coordinates identify the logical text box, not tight glyph bounds.",
      "required": [
        "x",
        "y",
        "width",
        "height",
        "text_x",
        "text_y",
        "text_width"
      ],
      "properties": {
        "x": {
          "type": "integer",
          "description": "Plate left relative to the layer origin"
        },
        "y": {
          "type": "integer",
          "description": "Plate top relative to the layer origin"
        },
        "width": {
          "type": "integer",
          "description": "Assembled plate width after minimum size and circle sizing"
        },
        "height": {
          "type": "integer",
          "description": "Assembled plate height after minimum size and circle sizing"
        },
        "text_x": {
          "type": "integer",
          "description": "Logical text origin inside the plate, including stroke and bearing placement"
        },
        "text_y": {
          "type": "integer",
          "description": "Logical text origin inside the plate"
        },
        "text_width": {
          "type": "integer",
          "description": "Resolved advance or constrained wrap width"
        }
      }
    },
    "TextRotationOrigin": {
      "type": "object",
      "description": "Actual pre-transform assembly center of ordinary raster text relative to the layer origin, in DSL pixels. Half-pixel centers are preserved.",
      "required": [
        "x",
        "y"
      ],
      "properties": {
        "x": {
          "type": "number"
        },
        "y": {
          "type": "number"
        }
      }
    }
  }
}