기술 문서

PDFlibPas의 Free Pascal EMF 벡터 가져오기

PDFlibPas는 향상된 메타파일(enhanced metafile)을 래스터화하지 않고 레코드 단위로 실제 PDF 페이지 콘텐츠로 변환하며, 가져온 차트나 CAD 도면이 어떤 확대 배율에서도 선명하게 유지되는 이유가 바로 여기에 있습니다. 이 변환기는 약 6500줄이고 VCL을 대상으로 작성되었으므로, 라이브러리에 Free Pascal 타깃이 추가되었을 때 이식 불가로 분류되어 스텁으로 대체되었습니다. 이 분류는 틀렸고, 틀린 방식 자체가 재작성을 결정하기 전에 의존성을 감사하는 요령을 보여 주는 유용한 교훈입니다

그 6500줄의 실제 VCL 사용 면적은 작았습니다. 픽셀 포맷, 스트림 저장, 핸들, 캔버스, 스캔라인을 위해 쓴 비트맵 클래스, 폭과 높이, 핸들을 위해 쓴 메타파일 클래스, 그리고 상수 두 개를 곁들인 색 타입이 전부입니다. 이것들은 모두 라이브러리 자체 그래픽스 유닛이 이미 제공하던 것이며, 그 유닛은 바로 non-VCL 빌드에 동등물을 제공하기 위해 존재합니다. 변환기는 VCL이 막고 있던 것이 전혀 아니었습니다. Free Pascal의 Windows 유닛이 막고 있었습니다

코드가 실제로 의존하는 축을 기준으로 나눕니다

따라서 이번 변경은 재구현이 아니었습니다. 조건문 하나였습니다. "VCL 없이 빌드될 때 스텁을 컴파일"에서 "Windows용으로 빌드하지 않을 때 스텁을 컴파일"로 바꾼 것입니다. 이것이 올바른 축이며, 이유를 말하면 차이가 명백해집니다. 향상된 메타파일은 Windows 컨테이너입니다. 변환기는 위에서 아래까지 Windows GDI 레코드용 파서입니다. 호스트 애플리케이션이 VCL을 쓰는지, 다른 위젯 세트를 쓰는지, 아무 위젯 세트도 쓰지 않는지는 그 레코드를 해석할 수 있는지와 아무 상관이 없고, 타깃이 Windows인지는 모든 것과 상관이 있습니다

올바른 축을 고르면 그 결과는 공짜로 따라옵니다. 이 라이브러리에서 Windows 플랫폼 심볼을 undefine하는 C++Builder 빌드는 예외를 던지는 스텁을 그대로 유지하며 이전과 정확히 같게 동작합니다. macOS는 GDI 레코드가 없으므로 스텁을 유지하는 것이 맞습니다. Delphi VCL 빌드는 그대로입니다. 그리고 non-VCL 위젯 세트를 쓰는 Windows 빌드는 아무도 구현할 필요가 없었던 벡터 EMF 가져오기를 부수 효과로 얻습니다. 실제 의존성에 맞춘 조건문은 플랫폼 작업을 한 줄 변경으로 바꾸고, 잘못된 의존성에 맞춘 조건문은 결코 일정에 오르지 않는 재작성으로 바꿉니다

EMF 가져오기 조건문이 VCL 여부에서 Windows 플랫폼으로 축을 다시 잡아 다른 곳의 스텁을 유지하면서 non-VCL Windows 빌드에 벡터 가져오기를 제공하는 모습
스텁 조건의 축을 Windows 플랫폼으로 다시 잡으면 기존 빌드 동작은 모두 유지되고 non-VCL Windows 타깃은 EMF 벡터 가져오기를 공짜로 얻습니다

Free Pascal의 공백은 선언이지 로직이 아닙니다

실제로 빠져 있던 것은 Delphi의 Windows 유닛이 제공하고 Free Pascal 유닛이 제공하지 않는 Win32 선언들이었습니다. 조건문을 변환기 곳곳에 흩뿌리는 대신 호환성 유닛 하나에 모으면서 파서의 가독성을 지켰습니다. 이 목록은 두 RTL 사이의 헤더 커버리지가 얼마나 고르지 않은지 보여 주므로 교훈적입니다. 메타파일 레코드 타입 상수 113개, 확장 텍스트 출력 플래그 2개, 그러디언트 채우기 모드 상수 3개, 핸들 테이블 포인터 타입, 그러디언트 버텍스와 프리미티브 레코드의 별칭, 그리고 Free Pascal이 아예 선언하지 않는 알파 블렌딩, 투명 blitting, 색 관리 모드를 다루는 레코드 타입 3개입니다

그중 어느 것도 개별적으로는 흥미롭지 않습니다. 전부 올라야 파서가 컴파일되고, 호환성 유닛은 헤더 문서와 유닛 단위로 diff할 수 있는 자연스러운 보금자리입니다

Free Pascal Windows 유닛에 없는 Win32 선언들을 EMF to PDF 벡터 변환기용 호환성 유닛 하나로 모은 모습
레코드 상수, 플래그, 별칭, 없는 레코드 타입 3개가 모두 헤더 문서와 diff할 수 있는 호환성 유닛 하나에 들어 있습니다

조용히 잘못된 그림을 그리는 하나

그 선언들 가운데 두 개는 단순히 없는 것이 아니라 존재하면서도 이 용도에는 틀립니다. 메타파일을 다룰 일이 평생 없을 사람이라도 기억할 가치가 있는 부분은 바로 여기입니다

Free Pascal은 브러시 생성 레코드 안에 실행 시점 브러시 구조체를 끼워 넣어 선언하고, 확장 펜 레코드 안에 실행 시점 펜 구조체를 끼워 넣어 선언합니다. 두 실행 시점 구조체 모두 hatch 멤버를 포인터 크기 정수로 선언하는데, 실제 GDI 호출에서는 이 멤버가 핸들을 담을 수 있기 때문입니다. 반면 메타파일은 항상 32비트 형태를 저장합니다. 레코드 레이아웃은 직렬화된 파일 포맷의 일부이므로 프로세스 비트니스에 따라 달라지지 않기 때문입니다

32비트 빌드에서는 둘이 일치하므로 아무 일도 일어나지 않습니다. Win64에서는 포인터 크기 멤버가 8바이트인데 파일에는 4바이트가 있으므로, hatch 멤버 뒤의 모든 필드가 잘못된 오프셋에서 읽힙니다. 예외도, 파스 오류도, 경고도 없습니다. 메타파일은 그저 잘못 그려질 뿐입니다. 잘못된 바이트에서 온 색, 잘못된 바이트에서 온 펜 폭, 그리고 구조체 레이아웃 버그가 아니라 렌더링 버그처럼 보이는 그림이 됩니다. Delphi가 정확히 이 이유로 두 구조체 모두 명시적 32비트 변형을 함께 제공하며, 호환성 유닛도 같은 방식으로 다시 선언합니다

EMF 브러시 레코드의 바이트 레이아웃에서 포인터 크기 hatch 필드가 Win64에서 뒤 필드를 4바이트 밀어내는 모습과 고정 32비트 레이아웃의 비교
직렬화된 레코드는 항상 4바이트 hatch를 저장하므로, 포인터 크기 실행 시점 구조체는 Win64에서 뒤 필드를 전부 조용히 잘못 읽습니다
// Win64에서 틀림: Hatch는 포인터 크기이지만 파일에는 32비트가
// 저장되고, 뒤따르는 모든 필드가 오류 없이 4바이트씩 밀립니다
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 64비트 프로세스에서는 8바이트
  end;

// 맞음: 직렬화된 레이아웃, 비트니스와 무관한 고정 폭
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // 메타파일에 저장되는 그대로, 항상 4바이트
  end;

일반 규칙은 다음과 같습니다. 실행 시점 API 인자로도, 직렬화된 필드 레이아웃으로도 등장하는 구조체는 선언이 두 개 필요하고, 직렬화 쪽은 전체를 고정 폭 타입으로 써야 합니다. 파일 포맷 안의 포인터 크기 멤버는 64비트 빌드를 기다리는 버그입니다

시그니처 차이는 호출 지점마다가 아니라 래퍼에 넣습니다

남은 차이는 평범한 시그니처 불일치였고, 흡수하는 방법은 호출 지점마다 조건문을 두는 대신 전달 래퍼를 두는 것입니다. 변환 행렬 결합 함수는 Delphi에서 참조 매개변수를 받는데 Free Pascal에서는 포인터를 받으므로, 래퍼는 참조를 받아 주소를 넘깁니다. 또한 두 소스 인자를 먼저 지역 변수로 복사합니다. 변환기에는 대상 행렬이 동시에 소스 중 하나인 호출 지점이 있는데, 읽으면서 쓰는 함수에 같은 주소를 두 번 넘기면 회전된 콘텐츠에서만 드러나는 미묘하게 틀린 변환이 나오기 때문입니다

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // 먼저 복사: 호출자가 Dest를 A나 B로 정당하게 넘길 수 있음
  SrcA := A;
  SrcB := B;
{$IFDEF FPC}
  Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
  Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;

사각형과 점 타입이 나머지 사례입니다. Free Pascal은 메타파일 사각형과 점 레코드를 일반 그래픽스 타입과 다른 타입으로 취급하므로, 여덟 군데 대입 지점이 동일한 레이아웃의 레코드 사이 명시적 캐스트를 필요로 했습니다. 두 컴파일러 모두 캐스트 형태를 받아들이므로 해당 지점들은 조건문을 전혀 갖지 않으며, 약간의 추함은 그만한 가치가 있습니다

Free Pascal 배포에서 달라지는 것

벡터 EMF 가져오기는 Free Pascal 아래 Windows에서 동작하며 Delphi 빌드와 같은 페이지 콘텐츠를 만들어 냅니다. 경로는 경로로, 그러디언트는 패턴 콘텐츠로, 텍스트는 텍스트로입니다. Windows 밖에서는 래스터 경로가 여전히 답이고, 이는 포트의 한계가 아니라 포맷의 한계입니다. 변환기가 값을 공급하는 좌표와 클리핑 상태는 콘텐츠 스트림 CTM 및 클리핑 트래커 문서에서 설명하고, 변환기가 내보내는 벡터 프리미티브는 벡터 그래픽스, 셰이더, 그러디언트에서 다룹니다

같은 기회를 찾아 자신의 코드베이스를 감사하고 있다면 유용한 연습은 이 일을 시작하게 한 바로 그것입니다. 의존한다고 생각하는 프레임워크에서 실제로 쓰는 멤버를 목록으로 적어 보는 것입니다. 답은 import 목록이 암시하는 것보다 훨씬 짧은 경우가 많고, 실제 제약은 보통 전혀 다른 곳에 있습니다. 디바이스 컨텍스트 기반 가져오기 경로 일반은 인쇄 미리보기와 디바이스 컨텍스트 문서에서 설명하고, 플랫폼과 툴체인 커버리지는 losLab PDF Developer Library 제품 페이지에 정리되어 있습니다