openapi: 3.1.0
info:
  title: PDF Renderer Service
  description: |
    HTML-to-PDF/PNG rendering service backed by a headless browser.
    Accepts a fully-formed HTML document and returns a rendered PDF or PNG
    depending on the `Accept` header.

    The service is stateless: each request contains everything needed to
    render. External resources referenced in the HTML (images, fonts, CSS)
    are fetched at render time, so keep them minimal and consider inlining
    assets as data URIs for determinism.
  version: 1.1.0

servers:
  - url: http://service-pdf:3000
    description: In-cluster service address

paths:
  /render:
    post:
      summary: Render HTML to PDF or PNG
      description: |
        Accepts an HTML document and returns the rendered output as binary.
        The response format is controlled by the `Accept` header:

        - `Accept: application/pdf` (default) — full paginated PDF
        - `Accept: image/png` — PNG snapshot of the first page

        The response body is the raw binary, not a JSON envelope.
      operationId: render
      parameters:
        - in: header
          name: Accept
          schema:
            type: string
            enum: [application/pdf, image/png]
            default: application/pdf
          description: |
            Desired output format. Defaults to `application/pdf`.
            Use `image/png` to get a PNG snapshot of the first page.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RenderRequest'
            examples:
              minimal:
                summary: Minimal PDF request
                value:
                  html: "<!DOCTYPE html><html><body><h1>Hello</h1></body></html>"
              transcript_pdf:
                summary: Transcript PDF with margins
                value:
                  html: "<!DOCTYPE html>..."
                  options:
                    format: A4
                    pdf:
                      printBackground: true
                      margin:
                        top: "12mm"
                        bottom: "12mm"
                        left: "10mm"
                        right: "10mm"
                  timeoutMs: 20000
              transcript_png:
                summary: First-page PNG thumbnail
                value:
                  html: "<!DOCTYPE html>..."
                  options:
                    format: A4
      responses:
        '200':
          description: Rendered successfully
          headers:
            Content-Length:
              description: Size of the output in bytes
              schema:
                type: integer
            Content-Type:
              description: MIME type of the output, matching the requested Accept format
              schema:
                type: string
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
        '400':
          description: Invalid request (missing HTML, malformed options, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: HTML payload too large
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Rendering failed (browser crash, timeout, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Service unavailable (browser unhealthy or queue full)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /healthz:
    get:
      summary: Service health check
      description: |
        Returns 200 if the service is alive and the browser instance is
        responsive. Used by orchestrators and clients with circuit breakers.
      operationId: healthCheck
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
        '503':
          description: Service is unhealthy (browser dead, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'

components:
  schemas:
    RenderRequest:
      type: object
      required:
        - html
      properties:
        html:
          type: string
          description: Complete HTML document to render. Must include doctype and html tags.
          minLength: 1
          maxLength: 10485760  # 10 MB cap; tune to your needs
          example: "<!DOCTYPE html><html><body>...</body></html>"
        options:
          $ref: '#/components/schemas/RenderOptions'
        timeoutMs:
          type: integer
          description: |
            Per-request render timeout in milliseconds. Caps total time
            spent on this render (page load + output generation). Server
            enforces an upper bound regardless of client value.
          minimum: 1000
          maximum: 120000
          default: 30000

    RenderOptions:
      type: object
      description: |
        Rendering options. `format` and `landscape` are shared across output
        types. Use the `pdf` sub-object for PDF-only options and `png` for
        PNG-only options. Type-specific fields take precedence over the
        shared fields when both apply.
      properties:
        format:
          type: string
          description: Page size preset. Overridden by explicit width/height in the type-specific sub-object.
          enum: [A4, A3, A5, Letter, Legal, Tabloid]
          default: A4
        landscape:
          type: boolean
          description: Render in landscape orientation.
          default: false
        pdf:
          $ref: '#/components/schemas/PdfOptions'
        png:
          $ref: '#/components/schemas/PngOptions'

    PdfOptions:
      type: object
      description: PDF-specific options. All fields optional.
      properties:
        width:
          type: string
          description: Page width with unit (e.g. "8.5in", "210mm"). Overrides format if set with height.
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
        height:
          type: string
          description: Page height with unit. Overrides format if set with width.
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
        printBackground:
          type: boolean
          description: |
            Print CSS backgrounds and colors. Required for designs that
            use background colors on bubbles, headers, etc.
          default: true
        margin:
          $ref: '#/components/schemas/Margin'
        scale:
          type: number
          description: Scale of the rendered content. 1.0 = 100%.
          minimum: 0.1
          maximum: 2.0
          default: 1.0
        preferCSSPageSize:
          type: boolean
          description: |
            If true, CSS @page rules in the HTML take precedence over
            format/width/height options.
          default: false

    PngOptions:
      type: object
      description: PNG-specific options. All fields optional.
      properties:
        width:
          type: integer
          description: |
            Viewport width in CSS pixels. Overrides format if set with height.
            At 96 dpi, A4 width ≈ 794 px and Letter width = 816 px.
          minimum: 1
        height:
          type: integer
          description: |
            Viewport height in CSS pixels, defining the first-page crop boundary.
            Overrides format if set with width.
          minimum: 1

    Margin:
      type: object
      description: Page margins. Each side optional; omitted sides default to "0".
      properties:
        top:
          type: string
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
          example: "12mm"
        bottom:
          type: string
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
          example: "12mm"
        left:
          type: string
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
          example: "10mm"
        right:
          type: string
          pattern: '^[0-9]+(\.[0-9]+)?(px|in|cm|mm)$'
          example: "10mm"

    Error:
      type: object
      required:
        - error
        - message
      properties:
        error:
          type: string
          description: Stable, machine-readable error code.
          enum:
            - invalid_request
            - html_too_large
            - render_timeout
            - render_failed
            - browser_unavailable
            - queue_full
        message:
          type: string
          description: Human-readable description. May change between versions; do not match on this.
          example: "HTML field is required"
        details:
          type: object
          description: Optional structured context (validation errors, etc.). Shape depends on error code.
          additionalProperties: true

    Health:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum: [ok, degraded, unhealthy]
        browser:
          type: object
          properties:
            connected:
              type: boolean
            uptimeMs:
              type: integer
              description: Time since the browser instance was launched.
        queue:
          type: object
          properties:
            depth:
              type: integer
              description: Number of renders waiting for a worker.
            inFlight:
              type: integer
              description: Number of renders currently executing.
