Catalog · How the catalog works
anydoc-dll
Windows DLL (C ABI) wrapper around firecrawl/anydoc, with a Delphi binding and VCL demo. Converts Office/OpenDocument/RTF/EPUB/CSV/PDF to GitHub Flavored Markdown.
- ★ 10 on GitHub
Anyone may press Interesting, signed in or not. It helps others find this project.
README
sc_anydoc
firecrawl/anydoc 을 Windows DLL(C ABI) 로 감싸고, Delphi 에서 바로 쓸 수 있게 만든 래퍼.
Word / PowerPoint / Excel / OpenDocument / RTF / EPUB / CSV / PDF 를 GitHub Flavored Markdown 으로 변환한다.
anydoc 이란
Firecrawl 이 Rust 로 만든 문서 변환 라이브러리다. MIT 라이선스.
?????? ????????? ??? ?????? ?????? ??? ????????? ?????? ??? ?????? ?????? ?????? ??? GFM ?????????????????? ??? ????????????포맷마다 파서가 따로 있지만 모두 하나의 공용 문서 모델로 수렴한 뒤 단일 시리얼라이저를 거친다. 그래서 어떤 포맷을 넣든 출력 마크다운의 스타일이 일관된다.
| 특징 | 내용 |
|---|---|
| 지원 포맷 | doc/docx/docm, ppt/pptx/pptm, xls/xlsx/xlsm/xlsb, odt/ods/odp, rtf, epub, csv, pdf |
| 포맷 감지 | 파일 내용의 시그니처로 판별한다. 확장자에 의존하지 않아 확장자가 틀려도 올바르게 처리된다 |
| 보존 범위 | 제목, 굵게/기울임, 링크, 표, 각주, 목록 |
| 속도 | 업스트림 기준 문서당 변환 중앙값 5ms 미만 |
| 외부 의존 | 없음. 순수 Rust 크레이트만 사용한다 |
Office 설치가 필요 없다. COM 자동화도, 백그라운드 Word 프로세스도 없이 바이트를 직접 파싱한다.
왜 이 래퍼를 만들었나
anydoc 은 Node.js / Python / WebAssembly 바인딩은 제공하지만 C ABI 바인딩이 없다. Delphi 를 비롯한 네이티브 언어에서 부를 방법이 없다는 뜻이다. 이 프로젝트가 그 빈자리를 메운다.
Windows 데스크톱 앱에서 문서를 텍스트로 뽑아내려면 보통 이런 선택지가 있는데, 각각 문제가 있다.
| 방식 | 문제 |
|---|---|
| MS Office COM 자동화 | Office 라이선스와 설치가 필요하고, 서버/무인 환경에서 불안정하다. 프로세스가 남거나 모달 대화상자에 걸린다 |
| 외부 CLI 프로세스 호출 | 프로세스 생성 비용, 임시 파일 관리, 표준출력 인코딩 문제가 따라붙는다 |
| 포맷별 파서 직접 구현 | docx/xlsx/pptx/pdf 를 각각 감당해야 한다. 현실적이지 않다 |
anydoc 은 이미 이 문제를 잘 푼 라이브러리이고, Rust 라 cdylib 로 빌드하면 그대로 DLL 이 된다. 얇은 C ABI 계층만 얹으면 Delphi 에서 함수 호출 한 번으로 끝난다. 외부 프로세스도, 임시 파일도, Office 설치도 필요 없다.
설계할 때 지킨 것
업스트림을 포크하지 않는다. anydoc 은 활발히 업데이트되는 프로젝트다. 포크하거나 소스를 벤더링하면 업스트림을 따라가는 일이 곧 유지보수 부담이 된다. 그래서 crates.io 의 크레이트를 의존성으로만 참조한다. 새 버전 반영이 cargo update 한 줄로 끝난다.
업스트림이 바뀌어도 래퍼가 깨지지 않게 한다. 아래 네 가지를 미리 처리해 뒀다.
| 위험 | 대응 |
|---|---|
| 포맷 enum 에 항목 추가·순서 변경 | 포맷을 서수가 아니라 문자열로 주고받는다. 새 포맷이 자동으로 노출된다 |
ConvertError 에 variant 추가 |
업스트림이 #[non_exhaustive] 로 선언했다. 와일드카드 arm 이 상태코드 99 로 흘려 컴파일이 깨지지 않게 한다. 메시지는 그대로 전달되어 정보 손실도 없다 |
| 포맷 해석 규칙 변경 | 업스트림 to_markdown() 과 같은 순서(내용 감지 → 확장자 폴백)를 따른다 |
| 어떤 코어가 들어있는지 모름 | build.rs 가 Cargo.lock 에서 읽어 DLL 에 새긴다. 런타임에 CoreVersion 으로 확인된다 |
32비트도 같이 지원한다. 확인해 보니 extern "C" export 이름에 32비트에서도 데코레이션이 붙지 않아, Delphi 선언 한 벌로 Win32/Win64 를 모두 커버할 수 있었다. NativeUInt 도 Rust usize 와 자동으로 폭이 맞는다.
요구 사항
| 도구 | 버전 | 비고 |
|---|---|---|
| Rust | 1.88 이상 | anydoc 코어 요구사항 (검증 환경: 1.97.1) |
| 타깃 | x86_64-pc-windows-msvc, i686-pc-windows-msvc |
rustup target add <triple> |
| MSVC 빌드 도구 | Visual Studio Build Tools | 링커(link.exe) 용 |
| Delphi | 13 (Studio 37.0) | 델파이 바인딩·데모를 쓸 때만 |
DLL 빌드
.\build_anydoc.ps1 # Win64
.\build_anydoc.ps1 -Platform Win32
.\build_anydoc.ps1 -Platform All # 양쪽 순차 빌드빌드 → ABI 검증 → 배포 순으로 진행하며, 검증에 실패하면 기존 DLL 을 덮어쓰지 않고 멈춘다.
| 옵션 | 설명 |
|---|---|
-Platform Win64|Win32|All |
대상 플랫폼 (기본 Win64) |
-DeployRoot <경로> |
배포 루트 (기본 C:\Works\lib\dll) |
-SkipDeploy |
빌드만 하고 배포하지 않음 |
-SkipVerify |
검증 생략 |
배포 위치
64?????? : C:\Works\lib\dll\sc_anydoc.dll
32?????? : C:\Works\lib\dll\win32\sc_anydoc.dll ??? ?????????, ????????? ??????32비트 프로세스는 64비트 DLL 을 로드할 수 없으므로 폴더로 분리한다. 이름이 같아 Delphi 선언을 플랫폼별로 나눌 필요가 없다.
빌드 결과
| Win64 | Win32 | |
|---|---|---|
| DLL 크기 | 7.7 MB | 6.3 MB |
| 의존 DLL | kernel32 / ntdll / bcryptprimitives / api-ms-win-core-synch |
동일 |
VC++ 재배포 패키지가 필요 없다. .cargo\config.toml 에 CRT 정적 링크(+crt-static)를 고정해 두어 VCRUNTIME140.dll 과 UCRT 의존이 사라진다. DLL 파일 하나만 복사하면 끝이다.
검증만 따로 실행
.\verify_dll.ps1 -Platform Win64
.\verify_dll.ps1 -Platform Win32 # 32비트 PowerShell 로 자동 재실행된다포인터 폭, 포맷 감지, 한글 UTF-8 왕복, 확장자 힌트 폴백, 오류 경로, 반복 변환/해제 300회를 확인한다.
업스트림 반영
.\update_upstream.ps1 # 최신 버전 확인만 (아무것도 바꾸지 않음)
.\update_upstream.ps1 -Apply # cargo update + 32/64 재빌드 + 검증 + 배포업스트림이 0.2.x 처럼 Cargo.toml 의 허용 범위를 벗어나면 자동 반영을 거부하고 안내만 출력한다. 이때는 릴리스 노트를 확인하고 Cargo.toml 의 버전을 직접 올린 뒤 다시 실행한다.
Delphi 에서 사용
delphi\src\SCAnydoc.pas 를 프로젝트에 추가한다. DLL 은 실행 파일 옆에 두거나 배포 폴더를 PATH 에 넣는다.
uses
SCAnydoc;
// 1) 파일 변환 — 파일 읽기를 델파이가 담당하므로 유니코드 경로에 안전하다
var LMarkdown := TSCAnydoc.ToMarkdownFile('C:\docs\보고서.docx');
// 2) 메모리 변환 — CSV 는 시그니처가 없어 확장자 힌트가 필수
var LMarkdown := TSCAnydoc.ToMarkdown(LBytes, 'csv');
// 3) 포맷만 감지 ('docx', 'pdf', 'excel' ... 실패 시 빈 문자열)
var LFormat := TSCAnydoc.DetectFormat(LBytes);
// 4) DLL 가용성 확인 (예외를 던지지 않는다)
if not TSCAnydoc.IsAvailable then
begin
ShowMessage('sc_anydoc.dll 을 찾을 수 없습니다.');
end;오류 처리
실패는 EAnydocError 로 올라온다. Status 로 원인을 구분하고, Detail 에는 코어가 준 원본(영문) 메시지가 담긴다.
try
LMarkdown := TSCAnydoc.ToMarkdownFile(LFileName);
except
on E: EAnydocError do
begin
if E.Status = asEncrypted then
begin
TSCLog.Instance.Warn('암호화 문서 건너뜀: ' + LFileName, 'DOC');
end
else
begin
TSCLog.Instance.Error(E.Message, 'DOC');
raise;
end;
end;
end;| 프로퍼티 | 내용 |
|---|---|
Status |
asUnsupported / asMalformed / asEncrypted / asResourceLimit / asMissingPart / asIo / asInvalidArg / asPanic / asUnknown |
Message |
한글로 조립된 표시용 메시지 |
Detail |
코어가 준 원본 메시지. 항상 영문이라 로케일에 의존하지 않는다 |
스레드
변환은 동기 blocking 이다. 큰 문서에서 UI 가 멈추므로, UI 프로젝트에서는 워커 스레드에서 호출하고 결과만 TThread.Queue 로 메인 스레드에 넘긴다.
TThread.CreateAnonymousThread(
procedure
var
LMarkdown: string; // 익명 메서드가 캡처하므로 var 블록에 선언
begin
LMarkdown := TSCAnydoc.ToMarkdown(LBytes, LExt);
TThread.Queue(nil,
procedure
begin
memResult.Lines.Text := LMarkdown;
end);
end).Start;DLL 은 전역 상태를 두지 않아 여러 스레드에서 동시에 호출해도 안전하다.
VCL 데모
delphi\demo\ 에 표준 VCL 샘플이 있다. UI 는 영문이다.
cd delphi\demo
.\build_demo.ps1 -Run # Win64 빌드 후 실행
.\build_demo.ps1 -Platform Win32sc_anydoc.dll 을 실행 파일 옆에 함께 복사하므로 PATH 설정 없이 실행된다. IDE 에서 열려면 AnydocDemo.dpr 을 File > Open Project 로 열면 .dproj 가 자동 생성된다.
데모가 보여주는 것:
- 문서 열기 / 창으로 드래그 앤 드롭(
WM_DROPFILES) / 실행 파일에 문서를 떨어뜨려 실행 - 변환을 워커 스레드에서 수행하고 UI 갱신만
TThread.Queue로 위임 FormCloseQuery로 변환 중 종료 차단 — Queue 로 넘긴 코드가 해제된 폼을 건드리는 AV 방지EAnydocError.Status/Detail로 실패 원인 표시- 상태바에 포맷·소요 시간·코어 버전 표시, 결과를 UTF-8
.md로 저장 (UTF-8 BOM 체크박스로 BOM 유무 선택, 기본은 BOM 없음) - Preview 탭에서
TEdgeBrowser(WebView2) 로 마크다운 렌더링 — 아래 참조 - PerMonitorV2 DPI 매니페스트 포함
마크다운 프리뷰
Preview.pas 가 TEdgeBrowser 를 프리뷰어로 운전한다. 마크다운 → HTML 변환은
marked 가 담당하며, sc_anydoc.dll 과 C ABI 는 이 기능에 관여하지 않는다.
- 셸 페이지는 한 번만 로드한다.
preview.html에marked.min.js를 통째로 인라인해(둘 다RCDATA리소스)NavigateToString으로 띄운 뒤, 이후 마크다운은PostWebMessageAsString으로 원문 그대로 넘긴다 — 재탐색도 JS 문자열 이스케이프도 없다 - 초기화는 Preview 탭을 처음 열 때까지 지연된다. 프리뷰를 쓰지 않으면 WebView2 생성 비용을 전혀 치르지 않는다
- 소스가 바뀌면 dirty 로만 표시하고, 실제 렌더는 탭이 보일 때 한 번에 한다
- CSP 로 외부 리소스를 차단한다. 인라인 스크립트·스타일과
https이미지만 허용 - 문서 안의 링크는 WebView 안에서 열지 않고
ShellExecute로 기본 브라우저에 넘긴다 - WebView2 런타임이 없으면 프리뷰 탭에 사유를 표시하고 변환·저장 기능은 그대로 동작한다. 런타임은 시스템에 설치되는 구성요소이며 exe 옆에 복사하는 파일이 아니다
build_demo.ps1 은 sc_anydoc.dll 과 함께 아키텍처에 맞는 WebView2Loader.dll(webview2\x64 / webview2\x86)을 실행 파일 옆에 복사한다. 다만 Delphi 13 은 loader 를 실행 파일에 정적 링크하므로 데모가 이 DLL 을 실제로 로드하지는 않는다 — 배포 묶음의 완결성을 위해 함께 둘 뿐이다.
C ABI
Delphi 외의 언어에서 쓸 때 참고. 모든 함수가 cdecl(extern "C") 이며, 32비트에서도 export 이름에 데코레이션이 붙지 않아 Win32/Win64 선언이 동일하다.
| 함수 | 설명 |
|---|---|
uint32_t anydoc_abi_version() |
ABI 버전 (현재 1) |
const char* anydoc_core_version() |
링크된 anydoc 코어 버전. 정적 문자열 — 해제 금지 |
const char* anydoc_wrapper_version() |
래퍼 버전. 해제 금지 |
uint32_t anydoc_pointer_size() |
4(Win32) 또는 8(Win64). 비트수 진단용 |
int32_t anydoc_detect_format(const uint8_t* data, size_t len, char** out_name, size_t* out_name_len) |
내용 기반 포맷 감지 |
int32_t anydoc_to_markdown(const uint8_t* data, size_t len, const char* format_hint, char** out_text, size_t* out_text_len, char** out_err, size_t* out_err_len) |
변환 |
void anydoc_free(char* p, size_t len) |
위 두 함수가 채운 버퍼 해제 |
상태 코드
| 값 | 의미 | 값 | 의미 |
|---|---|---|---|
| 0 | 성공 | 5 | 필수 구성요소 누락 |
| 1 | 지원하지 않는 포맷 | 6 | 입출력 오류 |
| 2 | 손상된 문서 | 7 | 잘못된 인자 |
| 3 | 암호화된 문서 | 8 | 엔진 내부 패닉 |
| 4 | 리소스 한계 초과 | 99 | 업스트림 신규 오류 |
규약
- 출력 문자열은 UTF-8, 길이가 함께 반환된다. NUL 종단도 붙지만 길이 사용을 권장한다
anydoc_free에는 함께 받은 길이를 그대로 넘긴다. Rust 할당자가 해제하므로 호출 측에서free/FreeMem하면 안 된다- 모든 진입점이
catch_unwind로 감싸여 있어 Rust 패닉이 FFI 경계를 넘지 않는다 format_hint는 확장자 문자열("docx",".pdf","csv"). 점은 있어도 없어도 된다. 포맷은 내용 감지를 먼저 시도하고 실패 시 힌트로 폴백하므로, 시그니처가 없는 CSV 는 힌트가 필수다- 파일 경로를 넘기는 API 는 일부러 두지 않았다. 호출 측이 바이트를 읽어 넘기면 Windows 유니코드 경로 인코딩 문제가 아예 발생하지 않는다
알려진 제약
- PDF 는 문서 모델(
to_document)을 지원하지 않는다 (업스트림 제약). 마크다운 변환만 가능하며, 스캔본/이미지 PDF 는 OCR 이 필요해Unsupported로 떨어진다. PDF 변환 품질은 업스트림이 쓰는pdf-inspector수준에 종속되므로, 실사용 샘플로 확인해 보는 것이 좋다 - Win32 메모리 한계 — 문서 전체를 메모리에 올려 파싱한다. 32비트 프로세스의 주소 공간(기본 2GB) 안에서 수백 MB 급 문서는 실패할 수 있다. 해당 시나리오가 있으면 호출 측에서 입력 크기 상한을 두는 편이 안전하다
- 업스트림
to_document(문서 모델 트리)는 C ABI 로 노출하지 않았다. 필요하면 JSON 직렬화 형태로 추가할 수 있다
구성
Cargo.toml crates.io ??? anydoc ??? ??????????????? ?????? (????????????????? ??????)
.cargo\config.toml ????????? CRT ?????? ?????? ??????
build.rs Cargo.lock ??? ?????? ????????? DLL ??? ??????
src\lib.rs C ABI ??????
build_anydoc.ps1 ?????? ??? ?????? ??? ??????
verify_dll.ps1 ABI ?????? (32????????? ?????? ?????????)
update_upstream.ps1 ???????????? ?????? ??? ??????
delphi\src\SCAnydoc.pas Delphi ?????????
delphi\demo\ VCL ?????? (dpr/pas/dfm/manifest/rc + build_demo.ps1)
Preview.pas TEdgeBrowser ??? ???????????? ???????????? ?????????
preview.html ????????? ??? ????????? (RCDATA)
marked.min.js ???????????? ????????? (RCDATA, MIT)
webview2\x64|x86\ WebView2Loader.dll (?????? ??? exe ????????? ??????)라이선스
MIT — LICENSE 참조.
업스트림 anydoc 크레이트도 MIT 다. 이 프로젝트가 만든 바이너리에 정적으로 링크되므로, 재배포할 때 해당 고지를 유지한다 — THIRD-PARTY-NOTICES.md 참조.

