space ocr
指南文章價格文件
developer

把一頁掃描件變成可以核對的 Markdown

把影像轉成 Markdown,並用 data.values、path 鍵控的 cells、review.flagged 與來源座標核對結果的開發指南。

8 分鐘閱讀· 2026-08-31

把掃描件轉成 Markdown,通常出於兩個理由,而這兩者要的東西並不完全一樣。

一是發布:你想把文件放進 Wiki、文件站或儲存庫,並且希望標題還是標題。二是餵給模型:多數 LLM 流程以 Markdown 為輸入,因為語法本身承載結構——## 告訴模型這裡是章節邊界,管線表格告訴它這些儲存格屬於同一列。純文字會把這些資訊丟掉。

失敗的方式也一樣。標題被壓成段落,文件站就成了一堵牆,檢索用的分塊也會在錯誤的位置斷開。而且在這兩種用途裡,你拿到的通常只是一整串 Markdown,沒有辦法追問這一行來自頁面的哪裡

「保留版式」真正要保留的東西

一個可用的 Markdown 轉換,需要正確完成四個彼此獨立的判斷:

  1. 閱讀順序 —— 多欄排版不能交錯。這是樸素 OCR 輸出最先崩掉的地方:引擎按偵測順序輸出段落,而不是人閱讀的順序。
  2. 區塊型別 —— 這一行是標題、清單項、引用,還是普通段落。在掃描件上,僅憑字級並不可靠。
  3. 表格結構 —— 哪些儲存格屬於同一列,哪一列是表頭,儲存格折成兩行時怎麼處理。
  4. 不掉東西 —— 沒人會注意到的失敗。段落悄悄消失了,Markdown 看上去依然完好。

第四點最棘手,因為它在產物裡是隱形的:掉了 5% 的轉換結果,讀起來完全通順。

一次呼叫,四層回應

POST /ocr/markdown 接收一張 URL 或 base64 點陣影像。需要結構、座標或複核介面時,請使用預設值 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[]

座標不在元素內,而在扁平的 data.cells 對照表中。elements[0] 指第一個元素;若第三個元素是表格,elements[2].cells[1] 就指它的第二個儲存格。data.review.flagged[].path 使用相同路徑語法,因此複核項目可以直接查表。

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.coverage 提供 vision_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,並把 path 與分塊一起保存,引用即可重新開啟原圖區域。用於文件站時,發布 data.values.markdown,同時把 elements、cells、review、image 作為複核旁路資料保留。

若不需要結構,請用閱讀順序純文字 OCR;覆蓋層實作可參考用來源座標驗證 OCR

把影像轉成可複核 Markdown 的步驟

  1. 準備單頁影像
    透過 URL 或 base64 提供點陣影像。PDF 應在呼叫 API 前逐頁轉成影像。
  2. 請求結構元素
    需要結構、來源座標或人工複核時,以 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],顯示原因,並按 data.image 框架繪製 box 或 quad。
  5. 檢查完整性後發布
    檢查 data.review.coverage 與 recovered blocks,完成必要複核後再發布或切分 Markdown。
Markdown 與元素回傳在哪裡?
組裝後的字串在 data.values.markdown。啟用 includeElements 後,只含內容的元素在 data.values.elements,座標與驗證中繼資料在扁平 data.cells 對照表中。
怎樣定位被標記的表格儲存格?
把 data.review.flagged 中的 path 直接用於 data.cells[flag.path]。表格儲存格路徑形如 elements[2].cells[1],再按 data.image 框架繪製 quad 或 box。
includeElements: false 會改變什麼?
預設值是 true。設為 false 後仍保留組裝好的 Markdown,但會省略 data.values.elements 與元素級 data.cells。
可以直接傳送 PDF 嗎?
OCR API 接收 JPEG、PNG、GIF、BMP、TIFF、WebP 點陣影像。請先把 PDF 每頁轉成影像;Web 應用可以將拖入的 PDF 點陣化。
如何確認費用?
目前每頁點數與方案政策以 /pricing 為準。處理失敗不收費;匯出政策應與 OCR 呼叫分開確認。
相關文章