{
  "openapi": "3.1.1",
  "info": {
    "title": "KuronekoServer CDN API",
    "version": "1.2.2",
    "summary": "KuronekoServer CDNの最適化・パッケージ配信API",
    "description": "KuronekoServer CDNのURLパラメータで画像を変換します。Face Detection（face_crop）のみ非対応です。\n\n公開URLでは `https://cdn.krnk.org/path/image.jpg?width=800&format=webp` のように元画像へ直接パラメータを追加できます。APIクライアントからは、同じ処理を行う `GET /?file=path/image.jpg` も利用できます。\n\n画像の処理順は、切り抜き、リサイズ、反転・回転、ぼかし・シャープ、明度・ガンマ・コントラスト、彩度・色相・色合い・セピア、出力形式・品質です。GETのほかHEADとOPTIONSにも対応します。\n\n`/npm/` 以下では、KuronekoServer CDN Package Libraryの検索とバージョン固定配信を利用できます。\n\nレスポンスは `Cache-Control`、`ETag`、`Last-Modified` に対応し、同じ条件を効率よく再利用できます。",
    "license": {
      "name": "KuronekoServer Terms of Service",
      "url": "https://krnk.org/terms"
    }
  },
  "servers": [
    {
      "url": "https://cdn.krnk.org",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Optimizer",
      "description": "画像変換およびCSS・JavaScript最適化"
    },
    {
      "name": "Packages",
      "description": "KuronekoServer CDN Package Libraryの検索とバージョン固定配信"
    }
  ],
  "security": [],
  "paths": {
    "/": {
      "parameters": [
        { "$ref": "#/components/parameters/File" },
        { "$ref": "#/components/parameters/Width" },
        { "$ref": "#/components/parameters/WidthAlias" },
        { "$ref": "#/components/parameters/Height" },
        { "$ref": "#/components/parameters/HeightAlias" },
        { "$ref": "#/components/parameters/Upscaling" },
        { "$ref": "#/components/parameters/Crop" },
        { "$ref": "#/components/parameters/AspectRatio" },
        { "$ref": "#/components/parameters/CropGravity" },
        { "$ref": "#/components/parameters/FocusCrop" },
        { "$ref": "#/components/parameters/Flip" },
        { "$ref": "#/components/parameters/Flop" },
        { "$ref": "#/components/parameters/Rotate" },
        { "$ref": "#/components/parameters/Blur" },
        { "$ref": "#/components/parameters/Sharpen" },
        { "$ref": "#/components/parameters/Brightness" },
        { "$ref": "#/components/parameters/Gamma" },
        { "$ref": "#/components/parameters/Contrast" },
        { "$ref": "#/components/parameters/Saturation" },
        { "$ref": "#/components/parameters/Hue" },
        { "$ref": "#/components/parameters/Tint" },
        { "$ref": "#/components/parameters/Sepia" },
        { "$ref": "#/components/parameters/Quality" },
        { "$ref": "#/components/parameters/QualityAlias" },
        { "$ref": "#/components/parameters/Format" },
        { "$ref": "#/components/parameters/FormatAlias" },
        { "$ref": "#/components/parameters/Minify" },
        { "$ref": "#/components/parameters/Compress" },
        { "$ref": "#/components/parameters/Safe" },
        { "$ref": "#/components/parameters/Accept" },
        { "$ref": "#/components/parameters/AcceptEncoding" },
        { "$ref": "#/components/parameters/IfNoneMatch" },
        { "$ref": "#/components/parameters/IfModifiedSince" }
      ],
      "get": {
        "tags": ["Optimizer"],
        "operationId": "optimizeFile",
        "summary": "画像またはアセットを最適化する",
        "description": "画像ではDynamic Images互換パラメータを使用します。CSS・JavaScriptでは `min=1`、`compress=1`、`safe=1` を使用します。複数の切り抜き方式を同時指定した場合は `focus_crop`、`crop`、`aspect_ratio` の順に優先されます。",
        "responses": {
          "200": { "$ref": "#/components/responses/OptimizedFile" },
          "304": { "$ref": "#/components/responses/NotModified" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "413": { "$ref": "#/components/responses/ProcessingLimit" },
          "415": { "$ref": "#/components/responses/UnsupportedMedia" },
          "500": { "$ref": "#/components/responses/ServerError" },
          "501": { "$ref": "#/components/responses/CodecUnavailable" }
        }
      },
      "head": {
        "tags": ["Optimizer"],
        "operationId": "inspectOptimizedFile",
        "summary": "変換後ファイルのヘッダーを取得する",
        "description": "GETと同じパラメータで、本文を返さずContent-Type、Content-Length、ETag、キャッシュ状態などを確認します。",
        "responses": {
          "200": { "$ref": "#/components/responses/OptimizedFileHeaders" },
          "304": { "$ref": "#/components/responses/NotModified" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "413": { "$ref": "#/components/responses/ProcessingLimit" },
          "415": { "$ref": "#/components/responses/UnsupportedMedia" },
          "500": { "$ref": "#/components/responses/ServerError" },
          "501": { "$ref": "#/components/responses/CodecUnavailable" }
        }
      }
    },
    "/npm": {
      "get": {
        "tags": ["Packages"],
        "operationId": "openPackageLibrary",
        "summary": "Package Libraryを開く",
        "description": "パッケージ検索画面を表示します。`/npm/package@1.2.3/`はパッケージの既定ファイルを解決します。ファイルを明示する形式と、`/npm/lib/library@1.2.3/path/to/file.js`も利用できます。",
        "responses": {
          "200": {
            "description": "Package LibraryのHTML画面",
            "content": {
              "text/html": {
                "schema": { "type": "string" }
              }
            }
          },
          "404": {
            "description": "Package Libraryの画面が見つからない"
          }
        }
      }
    },
    "/npm/search": {
      "get": {
        "tags": ["Packages"],
        "operationId": "searchPackages",
        "summary": "パッケージを検索する",
        "description": "複数のカタログを横断し、KuronekoServer CDNでそのまま利用できる固定バージョンURLを返します。同一バージョンで名前の末尾だけが `.js` の重複項目は、パッケージ名へ統合します。",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "パッケージ名、説明、キーワード。2〜80文字。",
            "schema": { "type": "string", "minLength": 2, "maxLength": 80 },
            "example": "three"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "返す結果数。",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "検索結果",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["service", "query", "total", "results"],
                  "properties": {
                    "service": { "type": "string" },
                    "query": { "type": "string" },
                    "total": { "type": "integer", "minimum": 0 },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "name",
                          "version",
                          "description",
                          "kind",
                          "url",
                          "ready"
                        ],
                        "properties": {
                          "name": { "type": "string" },
                          "version": { "type": "string" },
                          "description": { "type": "string" },
                          "kind": {
                            "type": "string",
                            "enum": ["package", "library"]
                          },
                          "url": { "type": "string" },
                          "ready": { "type": "boolean" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "502": {
            "description": "検索カタログを取得できない",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "File": {
        "name": "file",
        "in": "query",
        "required": true,
        "description": "ドキュメントルートからの相対パス。画像はJPEG、PNG/APNG、GIF、WebP、AVIF、BMP、TIFF、TGA、PBM、HEIC/HEIF、アセットはCSSとJavaScriptに対応します。`cache/`、絶対パス、`..`を含むパスは拒否されます。",
        "schema": { "type": "string", "minLength": 1 },
        "example": "images/sample.jpg"
      },
      "Width": {
        "name": "width",
        "in": "query",
        "description": "出力領域の最大幅。縦横比を維持します。",
        "schema": { "type": "integer", "minimum": 1, "maximum": 50000 },
        "example": 800
      },
      "WidthAlias": {
        "name": "w",
        "in": "query",
        "description": "`width`の短縮名。両方指定した場合は`width`を優先します。",
        "schema": { "type": "integer", "minimum": 1, "maximum": 50000 }
      },
      "Height": {
        "name": "height",
        "in": "query",
        "description": "出力領域の最大高さ。縦横比を維持します。",
        "schema": { "type": "integer", "minimum": 1, "maximum": 50000 },
        "example": 600
      },
      "HeightAlias": {
        "name": "h",
        "in": "query",
        "description": "`height`の短縮名。両方指定した場合は`height`を優先します。",
        "schema": { "type": "integer", "minimum": 1, "maximum": 50000 }
      },
      "Upscaling": {
        "name": "upscaling",
        "in": "query",
        "description": "既定では元画像より拡大しません。`resampling`時も出力は最大24メガピクセルです。",
        "schema": {
          "type": "string",
          "enum": ["off", "resampling"],
          "default": "off"
        }
      },
      "Crop": {
        "name": "crop",
        "in": "query",
        "description": "`width,height`でgravity基準、または`width,height,x,y`で座標指定の切り抜き。",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+,[0-9]+(?:,[0-9]+,[0-9]+)?$"
        },
        "example": "800,600"
      },
      "AspectRatio": {
        "name": "aspect_ratio",
        "in": "query",
        "description": "中央基準で指定比率へ切り抜きます。`auto`は無指定と同じです。",
        "schema": {
          "type": "string",
          "pattern": "^(?:auto|[0-9]+(?:\\.[0-9]+)?:[0-9]+(?:\\.[0-9]+)?)$"
        },
        "example": "16:9"
      },
      "CropGravity": {
        "name": "crop_gravity",
        "in": "query",
        "description": "2値形式の`crop`で使用する基準位置。",
        "schema": {
          "type": "string",
          "enum": [
            "center",
            "north",
            "south",
            "east",
            "west",
            "northeast",
            "northwest",
            "southeast",
            "southwest"
          ],
          "default": "center"
        }
      },
      "FocusCrop": {
        "name": "focus_crop",
        "in": "query",
        "description": "`width,height,x,y`。x/yはピクセル座標、または0.0〜1.0の相対座標です。Face Detectionは行いません。",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+,[0-9]+,[0-9]+(?:\\.[0-9]+)?,[0-9]+(?:\\.[0-9]+)?$"
        },
        "example": "800,600,0.5,0.5"
      },
      "Flip": {
        "name": "flip",
        "in": "query",
        "description": "上下反転。",
        "schema": { "type": "boolean", "default": false }
      },
      "Flop": {
        "name": "flop",
        "in": "query",
        "description": "左右反転。",
        "schema": { "type": "boolean", "default": false }
      },
      "Rotate": {
        "name": "rotate",
        "in": "query",
        "description": "時計回りの回転角度。",
        "schema": {
          "type": "integer",
          "enum": [-270, -180, -90, 0, 90, 180, 270],
          "default": 0
        }
      },
      "Blur": {
        "name": "blur",
        "in": "query",
        "description": "ぼかし強度。",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100,
          "default": 0
        }
      },
      "Sharpen": {
        "name": "sharpen",
        "in": "query",
        "description": "シャープ化。`true`または0〜100の強度を指定できます。",
        "schema": {
          "oneOf": [
            { "type": "boolean" },
            { "type": "integer", "minimum": 0, "maximum": 100 }
          ],
          "default": false
        }
      },
      "Brightness": {
        "name": "brightness",
        "in": "query",
        "description": "明るさ。負数で暗く、正数で明るくします。",
        "schema": {
          "type": "integer",
          "minimum": -100,
          "maximum": 100,
          "default": 0
        }
      },
      "Gamma": {
        "name": "gamma",
        "in": "query",
        "description": "ガンマ補正。",
        "schema": {
          "type": "integer",
          "minimum": -100,
          "maximum": 100,
          "default": 0
        }
      },
      "Contrast": {
        "name": "contrast",
        "in": "query",
        "description": "コントラスト。",
        "schema": {
          "type": "integer",
          "minimum": -100,
          "maximum": 100,
          "default": 0
        }
      },
      "Saturation": {
        "name": "saturation",
        "in": "query",
        "description": "彩度。",
        "schema": {
          "type": "integer",
          "minimum": -100,
          "maximum": 100,
          "default": 0
        }
      },
      "Hue": {
        "name": "hue",
        "in": "query",
        "description": "色相の回転量。",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100,
          "default": 0
        }
      },
      "Tint": {
        "name": "tint",
        "in": "query",
        "description": "6桁の16進カラー。`#`なしで指定します。白成分からの減算方式です。",
        "schema": { "type": "string", "pattern": "^[0-9a-fA-F]{6}$" },
        "example": "aaffff"
      },
      "Sepia": {
        "name": "sepia",
        "in": "query",
        "description": "セピア効果の強度。",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100,
          "default": 0
        }
      },
      "Quality": {
        "name": "quality",
        "in": "query",
        "description": "JPEG、WebP、AVIFの出力品質。PNGの圧縮レベルには使用しません。",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100,
          "default": 85
        }
      },
      "QualityAlias": {
        "name": "q",
        "in": "query",
        "description": "`quality`の短縮名。両方指定した場合は`quality`を優先します。",
        "schema": { "type": "integer", "minimum": 0, "maximum": 100 }
      },
      "Format": {
        "name": "format",
        "in": "query",
        "description": "出力形式。省略時はAcceptヘッダーに応じたWebP、または入力形式を使用します。",
        "schema": {
          "type": "string",
          "enum": ["jpeg", "jpg", "png", "gif", "webp", "avif"]
        }
      },
      "FormatAlias": {
        "name": "f",
        "in": "query",
        "description": "`format`の短縮名。両方指定した場合は`format`を優先します。",
        "schema": {
          "type": "string",
          "enum": ["jpeg", "jpg", "png", "gif", "webp", "avif"]
        }
      },
      "Minify": {
        "name": "min",
        "in": "query",
        "description": "CSSまたはJavaScriptを縮小します。画像では使用しません。",
        "schema": { "type": "integer", "enum": [0, 1], "default": 0 }
      },
      "Compress": {
        "name": "compress",
        "in": "query",
        "description": "クライアントがgzipを受け入れる場合にCSSまたはJavaScriptをgzip圧縮します。",
        "schema": { "type": "integer", "enum": [0, 1], "default": 0 }
      },
      "Safe": {
        "name": "safe",
        "in": "query",
        "description": "JavaScript縮小時に改行・末尾空白のみを整理する安全モード。",
        "schema": { "type": "integer", "enum": [0, 1], "default": 0 }
      },
      "Accept": {
        "name": "Accept",
        "in": "header",
        "description": "`format`省略時、`image/webp`を受け入れるクライアントにはWebPを優先します。",
        "schema": { "type": "string" },
        "example": "image/avif,image/webp,*/*"
      },
      "AcceptEncoding": {
        "name": "Accept-Encoding",
        "in": "header",
        "description": "`compress=1`のCSS・JavaScriptでgzipを有効にします。",
        "schema": { "type": "string" },
        "example": "gzip"
      },
      "IfNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "description": "以前のレスポンスのETag。一致時は304を返します。",
        "schema": { "type": "string" }
      },
      "IfModifiedSince": {
        "name": "If-Modified-Since",
        "in": "header",
        "description": "HTTP-date形式の更新日時。ETagがない場合に評価します。",
        "schema": { "type": "string" }
      }
    },
    "headers": {
      "ETag": {
        "description": "変換条件と元ファイルから生成したエンティティタグ。",
        "schema": { "type": "string" }
      },
      "LastModified": {
        "description": "元ファイルの最終更新日時。",
        "schema": { "type": "string" }
      },
      "OptimizerCache": {
        "description": "画像変換時は`BYPASS`、パススルー時は省略されます。",
        "schema": { "type": "string", "enum": ["BYPASS"] }
      },
      "CacheControl": {
        "description": "KuronekoServer CDNおよびクライアント向けのHTTPキャッシュ指示。",
        "schema": { "type": "string" }
      },
      "OptimizerStatus": {
        "description": "変換不要時は`passthrough`。",
        "schema": { "type": "string", "enum": ["passthrough"] }
      },
      "OriginalSize": {
        "description": "元ファイルのバイト数。",
        "schema": { "type": "integer", "minimum": 0 }
      },
      "CompressedSize": {
        "description": "画像変換後のバイト数。",
        "schema": { "type": "integer", "minimum": 0 }
      },
      "CompressionRatio": {
        "description": "元ファイル比の削減率。",
        "schema": { "type": "string" },
        "example": "42.5%"
      },
      "OutputFormat": {
        "description": "実際の画像出力形式。",
        "schema": {
          "type": "string",
          "enum": ["jpeg", "png", "gif", "webp", "avif"]
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error", "code"],
        "properties": {
          "error": { "type": "string" },
          "code": { "type": "integer" },
          "error_id": {
            "type": "string",
            "description": "予期しないサーバーエラーの問い合わせ用ID。"
          }
        }
      }
    },
    "responses": {
      "OptimizedFile": {
        "description": "変換済みファイル、または変換不要の元ファイル。",
        "headers": {
          "ETag": { "$ref": "#/components/headers/ETag" },
          "Last-Modified": { "$ref": "#/components/headers/LastModified" },
          "X-Optimizer-Cache": {
            "$ref": "#/components/headers/OptimizerCache"
          },
          "Cache-Control": { "$ref": "#/components/headers/CacheControl" },
          "X-Optimizer-Status": {
            "$ref": "#/components/headers/OptimizerStatus"
          },
          "X-Original-Size": { "$ref": "#/components/headers/OriginalSize" },
          "X-Compressed-Size": {
            "$ref": "#/components/headers/CompressedSize"
          },
          "X-Compression-Ratio": {
            "$ref": "#/components/headers/CompressionRatio"
          },
          "X-Output-Format": { "$ref": "#/components/headers/OutputFormat" }
        },
        "content": {
          "image/jpeg": { "schema": { "type": "string", "format": "binary" } },
          "image/png": { "schema": { "type": "string", "format": "binary" } },
          "image/gif": { "schema": { "type": "string", "format": "binary" } },
          "image/webp": { "schema": { "type": "string", "format": "binary" } },
          "image/avif": { "schema": { "type": "string", "format": "binary" } },
          "text/css": { "schema": { "type": "string" } },
          "application/javascript": { "schema": { "type": "string" } }
        }
      },
      "OptimizedFileHeaders": {
        "description": "変換結果のヘッダー。本文は返しません。",
        "headers": {
          "ETag": { "$ref": "#/components/headers/ETag" },
          "Last-Modified": { "$ref": "#/components/headers/LastModified" },
          "X-Optimizer-Cache": {
            "$ref": "#/components/headers/OptimizerCache"
          },
          "Cache-Control": { "$ref": "#/components/headers/CacheControl" },
          "X-Optimizer-Status": {
            "$ref": "#/components/headers/OptimizerStatus"
          },
          "X-Original-Size": { "$ref": "#/components/headers/OriginalSize" },
          "X-Compressed-Size": {
            "$ref": "#/components/headers/CompressedSize"
          },
          "X-Compression-Ratio": {
            "$ref": "#/components/headers/CompressionRatio"
          },
          "X-Output-Format": { "$ref": "#/components/headers/OutputFormat" }
        }
      },
      "NotModified": {
        "description": "条件付きリクエストのキャッシュが最新です。"
      },
      "BadRequest": {
        "description": "パラメータが不正です。`face_crop`を指定した場合もこの応答です。",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "NotFound": {
        "description": "ファイルが存在しないか、安全な公開パスではありません。",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "GET、HEAD、OPTIONS以外のメソッドです。",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "ProcessingLimit": {
        "description": "画像サイズまたはメモリ安全制限を超えています。入力画像は最大50メガピクセル、拡大出力は最大24メガピクセル、AVIF出力は最大4メガピクセルです。",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "UnsupportedMedia": {
        "description": "入力画像形式を識別できません。",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "ServerError": {
        "description": "変換中の予期しないエラーです。必要に応じて`error_id`を返します。",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "CodecUnavailable": {
        "description": "要求された入力または出力codecをサーバーが利用できません。",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      }
    }
  }
}
