Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works
Anything · Lazarus or Delphi
Random projectRandom project

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.

View on GitHub

Member rating

No ratings yet.

Sign in to rate this project.

README

sc_anydoc

English README

firecrawl/anydoc 을 Windows DLL(C ABI) 로 감싸고, Delphi 에서 바로 쓸 수 있게 만든 래퍼.

Word / PowerPoint / Excel / OpenDocument / RTF / EPUB / CSV / PDF 를 GitHub Flavored Markdown 으로 변환한다.


anydoc 이란

Firecrawl 이 Rust 로 만든 문서 변환 라이브러리다. MIT 라이선스.

pascal
?????? ????????? ??? ?????? ?????? ??? ????????? ?????? ??? ?????? ?????? ?????? ??? 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 빌드

powershell
.\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 검증 생략

배포 위치

pascal
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 파일 하나만 복사하면 끝이다.

검증만 따로 실행

powershell
.\verify_dll.ps1 -Platform Win64
.\verify_dll.ps1 -Platform Win32   # 32비트 PowerShell 로 자동 재실행된다

포인터 폭, 포맷 감지, 한글 UTF-8 왕복, 확장자 힌트 폴백, 오류 경로, 반복 변환/해제 300회를 확인한다.


업스트림 반영

powershell
.\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 에 넣는다.

pascal
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 에는 코어가 준 원본(영문) 메시지가 담긴다.

pascal
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 로 메인 스레드에 넘긴다.

pascal
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 는 영문이다.

powershell
cd delphi\demo
.\build_demo.ps1 -Run                  # Win64 빌드 후 실행
.\build_demo.ps1 -Platform Win32

sc_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 직렬화 형태로 추가할 수 있다

구성

pascal
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 참조.