openapi: 3.1.0
info:
  title: lens-server
  description: 'Reference HTTP rendering surface for the Print With Synergy lens-pdf viewer

    core. Renders PDF pages to composite RGB and per-ink separation PNGs, samples

    color/density at PDF coordinates, produces TAC heatmaps, and renders findings

    reports (HTML / PDF / annotated PDF / markup PDF).


    Bearer auth is optional: when `LENS_BEARER_TOKEN` is set, every route except

    the health/metrics probes requires `Authorization: Bearer <token>`. Errors are

    RFC 7807 Problem Details (`application/problem+json`).

    '
  version: 0.1.0
  license:
    name: AGPL-3.0-or-later
  contact:
    name: Print With Synergy
    url: https://github.com/printwithsynergy/lens-pdf
servers:
- url: /
  description: The lens-server instance.
tags:
- name: health
- name: sources
- name: rendering
- name: sampling
- name: reports
- name: inspection
security:
- bearerAuth: []
paths:
  /healthz:
    get:
      tags:
      - health
      summary: Liveness probe
      security: []
      responses:
        '200':
          description: Service is alive.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    const: ok
                  service:
                    type: string
                    const: lens-server
                  version:
                    type: string
      operationId: getHealthz
      description: Liveness probe
  /readyz:
    get:
      tags:
      - health
      summary: Readiness probe
      security: []
      responses:
        '200':
          description: Service is ready.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    const: ready
      operationId: getReadyz
      description: Readiness probe
  /metrics:
    get:
      tags:
      - health
      summary: Prometheus metrics
      security: []
      responses:
        '200':
          description: Prometheus exposition format.
          content:
            text/plain:
              schema:
                type: string
      operationId: getMetrics
      description: Prometheus metrics
  /jobs/{jobId}/source:
    post:
      tags:
      - sources
      summary: Register a PDF for a job (raw bytes or URL fetch)
      description: 'Pre-register a PDF before rendering. Send either raw PDF bytes

        (`application/pdf`) or a JSON body `{ "url": "https://…" }` (http/https

        only). Uploads are size-capped by `LENS_MAX_UPLOAD_MIB` (default 100 MiB).

        '
      parameters:
      - $ref: '#/components/parameters/JobId'
      requestBody:
        required: true
        content:
          application/pdf:
            schema:
              type: string
              format: binary
          application/json:
            schema:
              type: object
              required:
              - url
              properties:
                url:
                  type: string
                  format: uri
                  description: http(s) URL to fetch the PDF from.
      responses:
        '200':
          description: Source registered.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '400':
          $ref: '#/components/responses/Problem'
        '401':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
      operationId: postJobsJobidSource
  /jobs/{jobId}:
    delete:
      tags:
      - sources
      summary: Drop server-side state for a job
      description: 'Removes the job''s stored PDF + render cache. Hosts should pair this with a

        CDN purge of `Cache-Tag: job-{jobId}`.

        '
      parameters:
      - $ref: '#/components/parameters/JobId'
      responses:
        '204':
          description: Deleted.
        '401':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
      operationId: deleteJobsJobid
  /jobs/{jobId}/page/{pageNum}.png:
    get:
      tags:
      - rendering
      summary: Composite RGB page PNG
      parameters:
      - $ref: '#/components/parameters/JobId'
      - name: pageNum
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
      - $ref: '#/components/parameters/Dpi'
      responses:
        '200':
          description: Composite RGB raster.
          headers:
            Cache-Control:
              schema:
                type: string
              description: immutable, 1y
            Cache-Tag:
              schema:
                type: string
              description: job-{jobId}
          content:
            image/png:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
      operationId: getJobsJobidPagePagenumpng
      description: Composite RGB page PNG
  /jobs/{jobId}/channels:
    get:
      tags:
      - rendering
      summary: List ink-channel names on a page
      parameters:
      - $ref: '#/components/parameters/JobId'
      - name: page
        in: query
        required: true
        schema:
          type: integer
          minimum: 1
      - $ref: '#/components/parameters/Dpi'
      responses:
        '200':
          description: Channel names present on the page.
          content:
            application/json:
              schema:
                type: object
                properties:
                  channels:
                    type: array
                    items:
                      type: string
        '401':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
      operationId: getJobsJobidChannels
      description: List ink-channel names on a page
  /jobs/{jobId}/channel/{name}.png:
    get:
      tags:
      - rendering
      summary: Per-ink separation PNG (grayscale)
      description: White = no ink, black = full coverage. `name` must match a channel present on the page.
      parameters:
      - $ref: '#/components/parameters/JobId'
      - name: name
        in: path
        required: true
        schema:
          type: string
        description: Channel name (URL-encoded), e.g. Cyan or Pantone%20123%20C.
      - name: page
        in: query
        required: true
        schema:
          type: integer
          minimum: 1
      - $ref: '#/components/parameters/Dpi'
      responses:
        '200':
          description: Grayscale per-ink raster.
          content:
            image/png:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
      operationId: getJobsJobidChannelNamepng
  /jobs/{jobId}/color:
    get:
      tags:
      - sampling
      summary: Color sample (RGB + TAC) at a PDF point
      parameters:
      - $ref: '#/components/parameters/JobId'
      - name: page
        in: query
        required: true
        schema:
          type: integer
          minimum: 1
      - name: x
        in: query
        required: true
        schema:
          type: number
        description: PDF x coordinate (points; lower-left origin).
      - name: y
        in: query
        required: true
        schema:
          type: number
        description: PDF y coordinate (points; lower-left origin).
      - name: pageWidthPts
        in: query
        required: true
        schema:
          type: number
      - name: pageHeightPts
        in: query
        required: true
        schema:
          type: number
      - $ref: '#/components/parameters/Dpi'
      responses:
        '200':
          description: Color sample.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ColorSample'
        '401':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
      operationId: getJobsJobidColor
      description: Color sample (RGB + TAC) at a PDF point
  /jobs/{jobId}/density:
    post:
      tags:
      - sampling
      summary: Densitometer sample (per-ink density + TAC) at a PDF point
      parameters:
      - $ref: '#/components/parameters/JobId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - page
              - x
              - y
              - pageWidthPts
              - pageHeightPts
              - dpi
              properties:
                page:
                  type: integer
                  minimum: 1
                x:
                  type: number
                y:
                  type: number
                pageWidthPts:
                  type: number
                pageHeightPts:
                  type: number
                dpi:
                  type: integer
                  minimum: 36
                  maximum: 600
                tacLimit:
                  type: number
                  exclusiveMinimum: 0
                  default: 300
                  description: TAC threshold (%).
      responses:
        '200':
          description: Densitometer sample.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DensitometerSample'
        '401':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
      operationId: postJobsJobidDensity
      description: Densitometer sample (per-ink density + TAC) at a PDF point
  /jobs/{jobId}/tac.png:
    get:
      tags:
      - sampling
      summary: TAC heatmap PNG (RGBA)
      description: Transparent under `limit`; green→yellow→red gradient above it.
      parameters:
      - $ref: '#/components/parameters/JobId'
      - name: page
        in: query
        required: true
        schema:
          type: integer
          minimum: 1
      - name: dpi
        in: query
        required: true
        schema:
          type: integer
          minimum: 36
          maximum: 600
      - name: limit
        in: query
        required: true
        schema:
          type: number
          exclusiveMinimum: 0
        description: TAC threshold (%).
      responses:
        '200':
          description: RGBA heatmap raster.
          content:
            image/png:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
      operationId: getJobsJobidTacpng
  /render:
    post:
      tags:
      - reports
      summary: Render a findings report (HTML / PDF / annotated PDF / markup PDF)
      description: '`multipart/form-data` with a JSON `context` field (`RenderContext`) and,

        for `annotated_pdf` / `markup_pdf`, a `pdf` file part to overlay.

        '
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
              - context
              properties:
                context:
                  type: string
                  contentMediaType: application/json
                  contentSchema:
                    $ref: '#/components/schemas/RenderContext'
                  description: 'JSON-encoded `RenderContext` (the `format` field selects html|pdf|annotated_pdf|markup_pdf).'
                pdf:
                  type: string
                  format: binary
                  description: Original PDF — required for annotated_pdf / markup_pdf.
      responses:
        '200':
          description: Rendered report (HTML or PDF per the requested format).
          content:
            text/html:
              schema:
                type: string
            application/pdf:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/Problem'
        '401':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
        '500':
          $ref: '#/components/responses/Problem'
        '504':
          $ref: '#/components/responses/Problem'
      operationId: postRender
  /inspect:
    post:
      tags:
      - inspection
      summary: One-shot document inspection (page count, ink channels, page dims)
      description: 'Send the PDF as a multipart `file` field or a raw `application/pdf`

        body. Nothing is persisted — the document is inspected in a per-request

        temp dir and deleted before the response. This is the contract synergy''s

        `lens.inspect` node calls.

        '
      parameters:
      - name: page
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          default: 1
        description: Page number to inspect (1-based).
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
              - file
              properties:
                file:
                  type: string
                  format: binary
                  description: The PDF to inspect.
          application/pdf:
            schema:
              type: string
              format: binary
      responses:
        '200':
          description: Inspection metadata for the requested page.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InspectResult'
        '400':
          $ref: '#/components/responses/Problem'
        '401':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
        '500':
          $ref: '#/components/responses/Problem'
      operationId: postInspect
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Required when LENS_BEARER_TOKEN is set; otherwise the server runs open.
  parameters:
    JobId:
      name: jobId
      in: path
      required: true
      schema:
        type: string
        pattern: ^[a-zA-Z0-9_-]{1,128}$
      description: Job identifier (1–128 chars of [a-zA-Z0-9_-]).
    Dpi:
      name: dpi
      in: query
      required: false
      schema:
        type: integer
        minimum: 36
        maximum: 600
        default: 150
      description: Render resolution; clamped to [36, 600].
  responses:
    Problem:
      description: RFC 7807 Problem Details.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    InspectResult:
      type: object
      description: One-shot inspection metadata for a single page.
      required:
      - pageCount
      - page
      - inks
      - widthPx
      - heightPx
      - dpi
      properties:
        pageCount:
          type: integer
          description: Total pages in the document.
        page:
          type: integer
          description: The inspected page (1-based).
        inks:
          type: array
          items:
            type: string
          description: Ink-channel names (process + spot) reported by the separations pass.
        widthPx:
          type: integer
          description: Page width in pixels at the inspection DPI.
        heightPx:
          type: integer
          description: Page height in pixels at the inspection DPI.
        dpi:
          type: integer
          description: DPI of the inspection render.
    RenderContext:
      type: object
      description: Report-render request body (sent JSON-encoded in the multipart `context` field).
      required:
      - result_json
      - format
      properties:
        result_json:
          type: object
          additionalProperties: true
          description: The lens result payload to render (findings + page metadata).
        format:
          type: string
          enum:
          - html
          - pdf
          - annotated_pdf
          - markup_pdf
        branding:
          type: object
          additionalProperties: true
          description: Optional host branding (logo, colors, footer).
        detail_level:
          type: string
          enum:
          - executive
          - standard
          - comprehensive
        summary_page:
          type: string
          enum:
          - prepend
          - only
          - 'off'
        annotations:
          type: array
          description: Viewer annotations to overlay; used by annotated_pdf / markup_pdf.
          items:
            type: object
            additionalProperties: true
        comments_by_annotation:
          type: object
          additionalProperties: true
          description: Map of annotation id → viewer comments; used by annotated_pdf / markup_pdf.
    Problem:
      type: object
      properties:
        status:
          type: integer
        title:
          type: string
        detail:
          type: string
        instance:
          type: string
    ColorSample:
      type: object
      required:
      - x
      - y
      - rgb
      - hex
      properties:
        x:
          type: number
        y:
          type: number
        rgb:
          type: array
          items:
            type: integer
            minimum: 0
            maximum: 255
          minItems: 3
          maxItems: 3
        hex:
          type: string
        tac:
          type:
          - number
          - 'null'
          description: Total Area Coverage (%); null when no composite raster is available.
    DensitometerSample:
      type: object
      required:
      - x
      - y
      - dpi
      - channels
      - tac
      - tac_limit
      - limit_exceeded
      properties:
        x:
          type: number
        y:
          type: number
        dpi:
          type: integer
        channels:
          type: array
          items:
            type: object
            required:
            - name
            - percent
            properties:
              name:
                type: string
              percent:
                type: number
        tac:
          type: number
        tac_limit:
          type: number
        limit_exceeded:
          type: boolean
