space ocr
ガイド記事料金ドキュメント
developer

スキャンした 1 ページを、確かめられる Markdown にする

画像を Markdown に変換し、data.values の本文、path キーの cells、review.flagged、元画像座標を使って確認する実装ガイド。

8 分で読了· 2026-08-31

スキャンした書類を Markdown にしたい理由は、だいたい二つあります。そしてこの二つは求めるものが少し違います。

一つは公開。Wiki やドキュメントサイト、リポジトリに載せたい。そのとき見出しは見出しのままであってほしい。もう一つはモデルに読ませるため。多くの LLM パイプラインが Markdown を前提にしているのは、記法が構造を運ぶからです。## は「ここが節の切れ目だ」と伝え、パイプ表は「このセルたちは同じ行だ」と伝えます。素のテキストではその情報が消えます。

失敗の仕方も共通です。見出しが段落に潰れれば、ドキュメントサイトは一枚の壁になり、検索用のチャンクは変なところで切れます。そしてどちらの用途でも、返ってくるのはたいてい 1 本の Markdown 文字列だけで、この行はページのどこから来たのかを聞く手段がありません。

「レイアウト保存」が実際に守るべきもの

使える Markdown 変換は、独立した 4 つの判断を正しく行う必要があります。

  1. 読み順 — 多段組みで文章が入り混じらないこと。素の OCR 出力が最初に崩れるのがここで、エンジンは人が読む順ではなく検出順に段落を並べます。
  2. ブロック種別 — この行は見出しか、リスト項目か、引用か、ただの段落か。スキャンでは文字サイズだけでは判断できません。
  3. 表の構造 — どのセルが同じ行か、どの行がヘッダーか、セルが 2 行に折り返したときにどうなるか。
  4. 落とさないこと — 誰も気づかない失敗です。段落が静かに消えても、Markdown は「ちゃんとして見える」。

4 番目がいちばん厄介です。ページの 5% を落とした変換結果は、読む分には完璧に読めてしまいます。

1 回の呼び出し、4 つの応答レイヤー

POST /ocr/markdown は 1 枚のラスター画像を URL または base64 で受け取ります。構造・座標・確認 UI が必要なら、既定値でもある includeElements: true を使います。公開用 Markdown と確認用メタデータは応答内で分離されています。制限と全フィールドは API ドキュメント で確認できます。

1
2
3
4
5
6
7
8
curl -X POST https://api.space-ocr.com/ocr/markdown \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "https://example.com/report.jpg",
    "imageType": "url",
    "includeElements": true
  }'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
{
  "status": "success",
  "data": {
    "values": {
      "markdown": "# 四半期レポート\n\n売上は前年同期比で増加した。\n\n| 項目 | 金額 |\n| --- | --- |\n| 売上 | 12,000 |",
      "elements": [
        {
          "type": "heading",
          "level": 1,
          "text": "四半期レポート"
        },
        {
          "type": "paragraph",
          "text": "売上は前年同期比で増加した。"
        },
        {
          "type": "table",
          "rows": 2,
          "cols": 2,
          "cells": [
            {
              "row": 0,
              "col": 0,
              "header": true,
              "text": "項目"
            },
            {
              "row": 0,
              "col": 1,
              "header": true,
              "text": "金額"
            },
            {
              "row": 1,
              "col": 0,
              "header": false,
              "text": "売上"
            },
            {
              "row": 1,
              "col": 1,
              "header": false,
              "text": "12,000"
            }
          ]
        }
      ]
    },
    "cells": {
      "elements[0]": {
        "box": {
          "xmin": 36,
          "ymin": 21,
          "xmax": 314,
          "ymax": 39
        },
        "quad": [
          {
            "x": 36,
            "y": 21
          },
          {
            "x": 314,
            "y": 21
          },
          {
            "x": 314,
            "y": 39
          },
          {
            "x": 36,
            "y": 39
          }
        ],
        "verified": true,
        "review": null,
        "evidence": {
          "text_match": true,
          "source": "token_id"
        }
      },
      "elements[1]": {
        "box": {
          "xmin": 36,
          "ymin": 51,
          "xmax": 544,
          "ymax": 68
        },
        "quad": [
          {
            "x": 36,
            "y": 51
          },
          {
            "x": 544,
            "y": 51
          },
          {
            "x": 544,
            "y": 68
          },
          {
            "x": 36,
            "y": 68
          }
        ],
        "verified": true,
        "review": null,
        "evidence": {
          "text_match": true,
          "source": "token_id"
        }
      },
      "elements[2]": {
        "box": {
          "xmin": 36,
          "ymin": 86,
          "xmax": 387,
          "ymax": 137
        },
        "quad": [
          {
            "x": 36,
            "y": 86
          },
          {
            "x": 387,
            "y": 86
          },
          {
            "x": 387,
            "y": 137
          },
          {
            "x": 36,
            "y": 137
          }
        ],
        "verified": null,
        "review": null,
        "evidence": {}
      },
      "elements[2].cells[0]": {
        "box": {
          "xmin": 36,
          "ymin": 86,
          "xmax": 212,
          "ymax": 111
        },
        "quad": [
          {
            "x": 36,
            "y": 86
          },
          {
            "x": 212,
            "y": 86
          },
          {
            "x": 212,
            "y": 111
          },
          {
            "x": 36,
            "y": 111
          }
        ],
        "verified": true,
        "review": null,
        "evidence": {
          "text_match": true,
          "source": "token_id"
        }
      },
      "elements[2].cells[1]": {
        "box": {
          "xmin": 212,
          "ymin": 86,
          "xmax": 387,
          "ymax": 111
        },
        "quad": [
          {
            "x": 212,
            "y": 86
          },
          {
            "x": 387,
            "y": 86
          },
          {
            "x": 387,
            "y": 111
          },
          {
            "x": 212,
            "y": 111
          }
        ],
        "verified": false,
        "review": {
          "reasons": [
            "text_mismatch"
          ]
        },
        "evidence": {
          "text_match": false,
          "source": "token_id",
          "ocr_confidence": 0.71
        }
      },
      "elements[2].cells[2]": {
        "box": {
          "xmin": 36,
          "ymin": 111,
          "xmax": 212,
          "ymax": 137
        },
        "quad": [
          {
            "x": 36,
            "y": 111
          },
          {
            "x": 212,
            "y": 111
          },
          {
            "x": 212,
            "y": 137
          },
          {
            "x": 36,
            "y": 137
          }
        ],
        "verified": true,
        "review": null,
        "evidence": {
          "text_match": true,
          "source": "token_id"
        }
      },
      "elements[2].cells[3]": {
        "box": {
          "xmin": 212,
          "ymin": 111,
          "xmax": 387,
          "ymax": 137
        },
        "quad": [
          {
            "x": 212,
            "y": 111
          },
          {
            "x": 387,
            "y": 111
          },
          {
            "x": 387,
            "y": 137
          },
          {
            "x": 212,
            "y": 137
          }
        ],
        "verified": true,
        "review": null,
        "evidence": {
          "text_match": true,
          "source": "token_id"
        }
      }
    },
    "review": {
      "unit": "element",
      "total": 6,
      "boxed": 6,
      "verified": 5,
      "flagged": [
        {
          "path": "elements[2].cells[1]",
          "reasons": [
            "text_mismatch"
          ]
        }
      ],
      "by_reason": {
        "text_mismatch": 1
      },
      "coverage": {
        "recovered_blocks": 0,
        "vision_tokens": 40,
        "tokens_claimed": 40,
        "token_coverage": 1
      }
    },
    "image": {
      "width": 1654,
      "height": 2339
    }
  }
}

data.values.markdown は組み立て済み文字列、data.values.elements は内容だけの要素配列です。種類は見出し、段落、リスト、引用、コードブロック、区切り、表。表は rowscols、内容だけの cells[] を持ちます。

座標は要素の中ではなく、flat な data.cells にあります。elements[0] は最初の要素、elements[2].cells[1] は 3 番目の要素が表ならその 2 番目のセルです。data.review.flagged[].path も同じ文法なので、確認項目からそのまま lookup できます。

review-markdown-elements.js
1
2
3
4
5
6
7
8
9
10
11
12
const { data } = body;

for (const flag of data.review.flagged) {
  const cell = data.cells?.[flag.path];
  if (!cell) continue;

  console.log(flag.path, flag.reasons, cell.review?.reasons);
  drawQuad(cell.quad.map(({ x, y }) => ({
    x: (x / 1000) * data.image.width,
    y: (y / 1000) * data.image.height,
  })));
}

確認リストを原稿オーバーレイにする

独自のスコアしきい値ではなく、data.review.flagged から始めます。各項目の pathreasons を読み、data.cells[path]review.reasons を表示します。傾きに沿う枠には quad、軸に沿う計算には box を使います。

座標はどちらも 0〜1000。実際に読み取ったページを表す data.image.widthheight でピクセルへ戻します。verified: false は確認理由あり、true は理由なしで照合実行済み、null は理由なしでも照合対象がなかった状態です。evidence は診断材料であり、選ばれた文字が業務上正しい項目だという保証ではありません。

✓ Verified

欠落には明示的な手掛かりがあります。 data.review.coveragevision_tokenstokens_claimedtoken_coveragerecovered_blocks を返します。どの要素にも使われなかった token は、cell の evidence.source"unclaimed_tokens" の末尾段落として回収されます。完全性の確認に使い、API が定めていない合否しきい値は作らないでください。

elements を返さない選択

includeElements の既定値は true です。組み立て済み data.values.markdown だけでよい場合に限り false にします。その場合、data.values.elements と要素単位の data.cells は省略され、その応答から要素別ハイライトは作れません。

パイプラインでの使い分け

RAG では data.values.elements を見出し境界で chunk 化し、path を一緒に保存すると引用から原稿位置へ戻れます。ドキュメントサイトでは data.values.markdown を公開し、elements・cells・review・image を確認用 sidecar として保持します。

構造が不要なら 読み順を保つプレーンテキスト OCR、枠の実装は 元座標で OCR を確認する方法 を参照してください。

画像を確認可能な Markdown に変換する手順

  1. 1 ページを用意する
    URL または base64 のラスター画像を用意します。PDF は API 呼び出し前に 1 ページずつ画像化します。
  2. elements を要求する
    構造・座標・人の確認が必要なら includeElements: true で POST /ocr/markdown を呼びます。
  3. 応答レイヤーを保存する
    公開には data.values.markdown、構造処理には data.values.elements を使い、cells・review・image を確認用に保持します。
  4. flagged path を解決する
    data.review.flagged を順に読み、data.cells[flag.path] の理由と box・quad を data.image 上に表示します。
  5. 完全性を確認して公開する
    data.review.coverage と recovered blocks を確認し、必要なレビューを終えてから公開または chunk 化します。
Markdown と要素はどこに返りますか?
組み立て済み文字列は data.values.markdown。includeElements が有効なら内容だけの要素は data.values.elements、座標と検証情報は flat な data.cells に返ります。
確認対象の表セルを原稿上で見つけるには?
data.review.flagged の path を data.cells[path] にそのまま使います。表セルは elements[2].cells[1] のような path です。quad または box を data.image の基準で描画します。
includeElements: false では何が変わりますか?
既定値は true。false では組み立て済み Markdown は残りますが、data.values.elements と要素単位の data.cells は省略されます。
PDF を直接送れますか?
API が受け取るのは JPEG、PNG、GIF、BMP、TIFF、WebP のラスター画像です。PDF は 1 ページずつ画像化してから送ります。Web アプリはドロップした PDF を画像化できます。
料金はどう確認しますか?
現在のページ単位 credit と plan の条件は /pricing を確認してください。処理失敗は課金されません。export の条件は OCR 呼び出しと分けて確認します。
関連記事