xberg C API 实战:通过 bytes 输入将 PDF 提取为 Markdown 输出格式 后端AI 应用NLP【免费下载链接】xbergPolyglot document intelligence with a Rust core: extract text, metadata, images, tables, and structured data from 106 formats across 140 file extensions, plus code intelligence for 371 languages. Fifteen bindings, with CLI, REST API, and MCP server.项目地址https://gitcode.com/gh_mirrors/kr/xberg点击查看免费下载本文基于 xberg 仓库中自动生成的 C 语言契约测试片段docs-site/src/snippets-generated/c/contract/output_format_bytes_markdown.md及其配套 fixturefixtures/contract/output_format_bytes_markdown.json完整讲解如何在纯 C 环境中把 PDF 文件的原始字节送入 xberg 提取管道并通过config.output_format markdown得到 Markdown 化的文档内容。读完本文你将掌握xberg.h中xberg_extract_input_from_json/xberg_extraction_config_from_json/xberg_extract三个核心 FFI 函数的调用方式、句柄内存管理规则以及该契约测试对应的断言验证方法。一、场景与核心契约bytes 输入 Markdown 输出xberg 的提取入口统一接收ExtractInput提取输入与ExtractionConfig提取配置两类对象。在 C 绑定中二者都以 JSON 字符串构造。本文的契约测试验证的是其中一条关键路径输入形态kind bytes即直接以文件字节数组作为输入不经过磁盘路径或 URI 下载配置output_format markdown把提取后的content字段渲染为 Markdown样例文档pdf/fake_memo.pdfMIME 类型为application/pdf期望结果results[0].mime_type application/pdf、results[0].content长度不小于 10、results[0].metadata.output_format markdown。该契约由 fixture 文件 fixtures/contract/output_format_bytes_markdown.json 精确描述其assertions数组给出了三条可机器校验的断言assertions: [ { type: equals, field: results[0].mime_type, value: application/pdf }, { type: min_length, field: results[0].content, value: 10 }, { type: equals, field: results[0].metadata.output_format, value: markdown } ]也就是说只要输入是一份合法的 PDF 字节流且配置了 Markdown 输出返回结果中metadata.output_format必须如实回显为markdowncontent必须包含实质内容非空字符串。仓库中还提供了一个等价但走 URI 路径的对照契约 fixtures/contract/output_format_markdown.json其mock_responses通过模拟 HTTP 服务器提供同一份fake_memo.pdf断言字段完全一致——这印证了 bytes 与 URI 两种输入形态共享同一条提取与渲染管线。二、完整 C 示例逐段拆解原契约片段提供了一段可编译、可运行的 C 程序骨架。下面先给出完整代码再逐段解释其职责#include assert.h #include stdint.h #include stdio.h #include stdlib.h #include string.h #include xberg.h int main(void) { const char *input_json_base {\bytes\:\__ALEF_DOC_FILE_0__\,\config\:{\output_format\:\markdown\},\filename\:\fake_memo.pdf\,\kind\:\bytes\,\mime_type\:\application/pdf\}; FILE *input_file_0 fopen(pdf/fake_memo.pdf, rb); if (input_file_0 NULL) return EXIT_FAILURE; fseek(input_file_0, 0, SEEK_END); long input_size_0 ftell(input_file_0); if (input_size_0 0) { fclose(input_file_0); return EXIT_FAILURE; } rewind(input_file_0); uint8_t *input_bytes_0 malloc(input_size_0 0 ? (size_t)input_size_0 : 1); if (input_bytes_0 NULL) { fclose(input_file_0); return EXIT_FAILURE; } if (fread(input_bytes_0, 1, (size_t)input_size_0, input_file_0) ! (size_t)input_size_0) { free(input_bytes_0); fclose(input_file_0); return EXIT_FAILURE; } fclose(input_file_0); char *input_bytes_json_0 malloc((size_t)input_size_0 * 4 3); if (input_bytes_json_0 NULL) { free(input_bytes_0); return EXIT_FAILURE; } size_t input_offset_0 0; input_bytes_json_0[input_offset_0] [; for (long i 0; i input_size_0; i) { input_offset_0 (size_t)snprintf(input_bytes_json_0 input_offset_0, 5, %s%u, i 0 ? : ,, input_bytes_0[i]); } input_bytes_json_0[input_offset_0] ]; input_bytes_json_0[input_offset_0] \0; free(input_bytes_0); const char *input_marker_0 \__ALEF_DOC_FILE_0__\; const char *input_position_0 strstr(input_json_base, input_marker_0); if (input_position_0 NULL) { free(input_bytes_json_0); return EXIT_FAILURE; } size_t input_prefix_0 (size_t)(input_position_0 - input_json_base); size_t input_json_size_0 strlen(input_json_base) - strlen(input_marker_0) strlen(input_bytes_json_0) 1; char *input_json_0 malloc(input_json_size_0); if (input_json_0 NULL) { free(input_bytes_json_0); return EXIT_FAILURE; } snprintf(input_json_0, input_json_size_0, %.*s%s%s, (int)input_prefix_0, input_json_base, input_bytes_json_0, input_position_0 strlen(input_marker_0)); free(input_bytes_json_0); XBERGAlefHandle input_handle xberg_extract_input_from_json(input_json_0); free(input_json_0); XBERGAlefHandle config_handle xberg_extraction_config_from_json({\output_format\:\markdown\}); XBERGAlefHandle result xberg_extract(input_handle, config_handle); xberg_extract_input_free(input_handle); xberg_extraction_config_free(config_handle); xberg_extraction_result_free(result); return EXIT_SUCCESS; }1. 定义输入模板 JSON程序先定义一段带占位符的ExtractInputJSON 模板{ bytes: __ALEF_DOC_FILE_0__, config: { output_format: markdown }, filename: fake_memo.pdf, kind: bytes, mime_type: application/pdf }各字段语义如下字段值含义kindbytes输入形态为原始字节数组FFI 据此分派到 bytes 提取路径bytes__ALEF_DOC_FILE_0__占位符运行期替换为 PDF 字节序列的 JSON 数组表示mime_typeapplication/pdf显式声明文档类型帮助提取器按 PDF 解析filenamefake_memo.pdf源文件名会进入结果元数据config{output_format: markdown}提取配置内联在输入 JSON 中指定 Markdown 输出注意config也可以独立以ExtractionConfigJSON 传入见下文第三步两种方式在 FFI 层最终汇合于同一个 RustExtractionConfig类型。2. 读取文件并转换为 JSON 字节数组C 端无法直接把二进制指针嵌入 JSON 字符串因此示例采用了两段式转换fopen(pdf/fake_memo.pdf, rb)fseek/ftell/rewind取得文件长度并整体读入uint8_t *input_bytes_0分配size * 4 3的缓冲区用snprintf逐字节输出%u并逗号分隔最终形如[37,80,68,70,45,49,46,51,10,...]的十进制字节数组文本前后用[、]包裹。这段代码与 fixture 中input.bytes字段的存储方式完全一致——fixtures/contract/output_format_bytes_markdown.json 中就是以逗号分隔的十进制字节序列保存这份 PDF 的。也就是说bytes字段接受的是JSON 整数数组形式的字节流。3. 占位符替换拼出最终输入 JSON拿到字节数组文本后用strstr定位模板中的__ALEF_DOC_FILE_0__占位符计算前缀长度再通过snprintf把「前缀 字节数组 后缀」拼装成完整的输入 JSON 字符串。这一步是契约测试的通用机制__ALEF_DOC_FILE_N__标记表示第 N 个文档文件占位在真实应用中你完全可以直接把字节数组内联进 JSON无需走占位符替换。4. 调用 FFI构造输入、构造配置、执行提取三个核心调用构成完整提取链路XBERGAlefHandle input_handle xberg_extract_input_from_json(input_json_0); XBERGAlefHandle config_handle xberg_extraction_config_from_json({\output_format\:\markdown\}); XBERGAlefHandle result xberg_extract(input_handle, config_handle);xberg_extract_input_from_json把输入 JSON 反序列化为 RustExtractInput注册到句柄表并返回XBERGAlefHandlexberg_extraction_config_from_json同理构造ExtractionConfigxberg_extract同时接收两个句柄在内部完成异步提取并返回结果句柄ExtractionResult。5. 释放句柄内存管理的对称性示例在调用后立即释放全部句柄xberg_extract_input_free(input_handle); xberg_extraction_config_free(config_handle); xberg_extraction_result_free(result);这是 xberg C FFI 的句柄生命周期铁律每个*_from_json/*_extract*返回的非零句柄都必须由对应的*_free函数释放且释放顺序无严格限制但建议先释放不再需要的中间句柄。本示例在构造完input_json_0后立即free该 JSON 字符串随后在拿到input_handle后再释放体现了「FFI 句柄生命周期独立于调用方字符串缓冲区」的设计。三、FFI 底层实现JSON 反序列化与句柄管理上述函数全部声明于 cbindgen 自动生成的头文件 crates/xberg-ffi/include/xberg.h实现位于 crates/xberg-ffi/src/lib.rs。结合源码可以确认以下几点实现事实输入构造xberg_extract_input_from_json 先对指针做空指针与 UTF-8 校验再执行serde_json::from_str::xberg::ExtractInput解析失败时设置错误码并返回0零句柄表示失败。与之对称的 xberg_extract_input_free 从句柄表移除该输入。配置构造xberg_extraction_config_from_json 反序列化为xberg::ExtractionConfig对应释放函数为 xberg_extraction_config_free。提取执行xberg_extract 会先校验两个句柄的类型ExtractInput与ExtractionConfig通过后取得句柄内部值的克隆再借助全局 FFI tokio runtime 执行xberg::extract(input_rs, config_rs).await。这意味着该函数是阻塞式的——内部block_on会一直等待异步管道完成C 调用方无需自行管理异步状态。结果释放xberg_extraction_result_free 回收结果句柄。markdown字符串到枚举值的映射发生在 Rust 核心层的配置解析中。在 crates/xberg/src/api/handlers.rs 可以看到markdown crate::core::config::OutputFormat::Markdown的对应关系即配置 JSON 中的output_format字段最终落到OutputFormat::Markdown枚举变体。四、Markdown 输出格式的行为细节output_format控制的是结果content字段的渲染格式它独立于result_formatelement-based / document structure。关于 Markdown 输出仓库文档 docs-site/src/content/docs/guides/output-formats.mdx 给出了几条重要约定直接关系本契约测试的语义默认是 PlainOutputFormat::Plain默认返回最小格式化的原始文本Markdown则是 Markdown 化内容。本测试显式设置markdown因此断言metadata.output_format markdown。结构启发式契约output_format Plain会跳过结构启发式、直接返回原始文本任何非Plain的格式包括markdown都会对 PDF 文本块运行基于几何的结构启发式标题、列表等。因此对扫描版 PDF 启用 OCR 时markdown输出相比plain更容易恢复标题层级——但恢复程度取决于 OCR 后端文档实测显示 Tesseract 对标题/列表项的恢复最好Sceptre 较少PaddleOCR 不提升标题启用 ML 版面检测能提升各类后端的标题命中数。表格锚点配合table_anchors trueMarkdown 渲染器会在每个表格的 Markdown 块前输出[TABLE:{table_id}]标记便于下游用正则把结构化表格数据拼回渲染文本。table_anchors仅在output_format为Markdown或Djot时生效默认关闭。因此本测试断言content长度不小于 10正是为了确保 Markdown 渲染确实产出了实质内容而非空串或仅占位符。content字段还会伴随pagesPDF 分页内容、tables结构化表格、images图片元数据等统一输出结构。五、从契约到实战C 调用方的落地要点把这段契约测试迁移到真实业务建议遵循以下要点MIME 声明先行kind bytes时必须提供正确的mime_type如application/pdf否则提取器无法分派。若不确定类型可在调用前用 xberg 的 MIME 检测能力推断。字节数组直接内联实际代码中无需占位符机制直接把读取到的文件字节转换为 JSON 整数数组即可对超大文件可考虑分块或改用 URI 输入kind uri避免 JSON 体积膨胀。错误处理所有 FFI 函数返回0即表示失败可通过 xberg 的 last-error 机制查询错误详情句柄释放函数对0句柄是安全的实现中if handle ! 0才执行移除。与其它形态对照同样的断言在 URI 输入路径的 fixtures/contract/output_format_markdown.json 中保持一致说明「输入形态不同、输出契约相同」——bytes 适合本地文件与内存数据URI 适合远程文档。六、验证与回归为什么需要这个契约测试该片段由 alef 工具自动生成文件头注释声明 This file is auto-generated by alef — DO NOT EDIT.frontmatter 中标明language: c、target: c、requires: []、side_effect: safe。它属于 e2e 契约测试体系的一部分为每种绑定语言生成等价的调用代码验证同一份输入在不同语言绑定下产生一致的输出契约。本用例验证的是 bytes 提取 API 与 Markdown 输出格式的组合side_effect: safe表明该测试只做提取、不产生外部副作用。对维护者而言这段测试同时守护了三层不变量输入 JSON 的字段契约kind/bytes/mime_type/filename、配置解析output_format字符串映射、以及输出断言MIME 回显、内容非空、格式元数据回显。任何一层被破坏例如配置解析改名、bytes 路径丢失 MIME 信息、Markdown 渲染返回空串该 C 契约测试都会立即失败。七、进一步探索完整 C 头文件与全部 FFI 签名crates/xberg-ffi/include/xberg.h、crates/xberg-ffi/src/lib.rsbytes 契约 fixture含完整 PDF 字节与断言fixtures/contract/output_format_bytes_markdown.jsonURI 对照契约fixtures/contract/output_format_markdown.json输出格式全景文档Plain/Markdown/Djot/Html/Json 等docs-site/src/content/docs/guides/output-formats.mdx配置解析映射源码crates/xberg/src/api/handlers.rs赞分享后端AI 应用NLP【免费下载链接】xbergPolyglot document intelligence with a Rust core: extract text, metadata, images, tables, and structured data from 106 formats across 140 file extensions, plus code intelligence for 371 languages. Fifteen bindings, with CLI, REST API, and MCP server.项目地址https://gitcode.com/gh_mirrors/kr/xberg点击查看免费下载相关推荐Xberg C FFI 实战在 extract_batch 中混用 Bytes 与 URL 输入并共享同一输出信封Xberg C FFI 实战在 extract_batch 中混用 Bytes 与 URL 输入并共享同一输出信封 本文基于 Xberg 仓库中由 alef后端AI 应用NLPXberg C 绑定实战通过 FFI 提取 gzip 压缩传输的远程文档Xberg C 绑定实战通过 FFI 提取 gzip 压缩传输的远程文档 本文围绕仓库中自动生成的 C 语言示例 url_gzip_encoded_docum后端AI 应用NLPPDF补丁丁完全免费的PDF全能工具箱轻松解决您的PDF编辑难题PDF补丁丁完全免费的PDF全能工具箱轻松解决您的PDF编辑难题 还在为PDF文件的编辑、合并、书签管理而烦恼吗PDF补丁丁作为一款功能强大的开源PDF工后端AI 应用NLP上一篇如何快速掌握Claude Code Hooks面向新手的完整入门指南下一篇GLava核心功能解析让你的音乐拥有动态视觉灵魂创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考