기술 문서

PDFlibPas EPS 임포트: PostScript와 PDF의 간극 메우기

PDF 페이지에 놓인 EPS 로고는 외곽선이 사라진 채로 나오거나, 한 색짜리 마크가 제 네거티브로 인쇄되거나, Æ가 á로 바뀌어 나올 수 있습니다. 각 증상은 PostScript와 PDF 그래픽스 의미론이 어긋나는 자리를 표시합니다. 그래서 Delphi용 PDFlibPas EPS 임포트는 PostScript 프로그램을 해석하고 각 차이를 네이티브 PDF 연산자로 씁니다

프로그램을 해석하거나 그대로 통과시키기

PostScript는 PDF 안에 두 가지 방법으로 앉을 수 있고, 화면에 무언가를 보여 주는 쪽은 둘 중 하나뿐입니다. ISO 32000-1 §8.8.2는 PostScript XObject를 정의합니다. 출력 디바이스가 PostScript 프린터일 때만 바이트가 해석 없이 인쇄 작업으로 복사되는 /Subtype /PS 스트림입니다. 그 밖의 어디서도 효과가 없고, §8.8 개요는 PostScript XObject를 쓰지 말라고 합니다. PDF/A는 아예 배제합니다. TPDFlib.NewPostScriptXObject과 DrawPostScriptXObject은 그 패스스루 꼴을 쓰고, ImportEPSFromFile과 DrawPostScript는 프로그램을 인터프리터로 돌려 평범한 패스와 인라인 이미지, 텍스트를 내놓습니다. 아트워크는 인터프리터로 보내고, PS XObject는 인쇄기나 프로덕션 프린터 앞단의 래스터 이미지 프로세서, 곧 한 인쇄소의 RIP을 겨냥한 조각에만 남겨 두세요. EPS 해석은 EMF 벡터 임포트와도 다른 일입니다. 그쪽 입력은 다시 재생할 GDI 레코드 목록이지만, EPS 파일은 프로그램입니다

procedure AddPrintRoomHook(PDF: TPDFlib);
var
  PSRef: Integer;
begin
  // /Subtype /PS 스트림으로 있는 그대로 저장되고 Do로 참조됩니다
  // PostScript 프린터에서만 실행됩니다. 뷰어는 아무것도 그리지 않습니다
  PSRef := PDF.NewPostScriptXObject(
    'userdict /PrintRoomMark known { PrintRoomMark } if');
  if PSRef <> 0 then
    PDF.DrawPostScriptXObject(PSRef);
end;
PostScript 실행 모델과 PDF가 갈라지는 지점, 그리고 연산자를 한 줄씩 번역했을 때 나오는 결과
PostScript 행동PDF 행동문자 그대로의 번역 결과
gsave / grestore는 현재 패스와 현재 포인트를 저장하고 복원합니다q / Q는 패스를 그대로 두고, 그리기가 끝냅니다(§8.5.1)grestore 뒤의 외곽선이나 이어지는 선분이 사라집니다
clip은 나중 그리기를 위해 현재 패스를 지켜 둡니다W 뒤에는 보통 n인 그리기 연산자가 따르고, 그것이 패스를 끝냅니다clip 뒤의 스트로크는 그릴 것이 없습니다
ImageMatrix는 사용자 공간을 이미지 공간으로 사상합니다모든 이미지는 단위 정사각형을 채우고 첫 행이 위에 옵니다(§8.9.4)아래서 위로나 회전된 이미지는 뒤집히거나 엉뚱한 곳에 놓입니다
true imagemask는 1 비트를 그립니다기본 Decode [0 1]의 스텐실은 0 비트를 그립니다(§8.9.6.2)한 색 로고가 제 네거티브로 인쇄됩니다
로만 Standard-14 폰트는 기본이 StandardEncoding입니다StandardEncoding이라는 이름은 특별한 의미가 없습니다(§9.6.6.1)악센트와 따옴표, 합자가 글자를 바꿉니다
%%BoundingBox와 %%HiResBoundingBox 코멘트가 아트 영역을 정의합니다배치는 라이터가 내놓는 행렬 그대로입니다아트가 의도한 박스에서 밀리거나 스케일이 어긋납니다

grestore 뒤에 외곽선은 어디로 갔는가?

외곽선이 사라진 것은 PDF가 패스를 저장해 둔 적이 없기 때문입니다. ISO 32000-1 §8.5.1은 한 문장으로 정리합니다. "PDF에서는(PostScript와 달리) 현재 패스가 그래픽스 상태의 일부가 아닙니다", 그리고 패스가 그려진 뒤에는 m이나 re가 새 패스를 시작하기 전까지는 현재 패스가 없습니다. PostScript 생산자는 반대 규칙에 의지합니다. 외곽선 달린 채워진 도형을 그리는 평범한 방법은 gsave fill grestore stroke입니다. 채움이 저장된 패스 사본을 소비하고, grestore가 패스를 되돌려 놓고, 스트로크가 같은 기하를 그립니다. gsave를 q로, fill을 f로, grestore를 Q로 옮기면 스트로크는 빈 패스에 도착합니다. 도형은 채워지고 외곽선은 사라집니다. 복원된 패스를 이어가는 grestore 뒤의 lineto도 같은 식으로 실패합니다. 빠진 선분으로요

Delphi용 PDFlibPas EPS 임포트에서 PostScript gsave fill grestore stroke와 문자 그대로의 q f Q S 번역을 비교하는 타임라인
문자 그대로의 q f Q 대응은 채움에서 패스를 끝냅니다. 그래서 PDFlibPas는 사적 PostScript 패스를 지켜 두고 grestore 뒤의 스트로크는 여전히 제 기하를 찾습니다
uses
  SysUtils, PDFlibrary;

const
  // 외곽선 달린 채워진 삼각형: 채움은 gsave/grestore 안에서 돌고,
  // grestore 뒤의 스트로크는 복원된 패스를 재사용합니다
  TriangleEPS =
    '%!PS-Adobe-3.0 EPSF-3.0'#10 +
    '%%BoundingBox: 0 0 200 120'#10 +
    'newpath 20 20 moveto 180 20 lineto 100 100 lineto closepath'#10 +
    'gsave 0.85 setgray fill grestore'#10 +
    '3 setlinewidth 0 setgray stroke'#10;

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);   // Left와 Top을 왼쪽 위 모서리에서 셉니다
    // 0인 Width와 Height는 200 x 120 바운딩 박스 크기를 지킵니다
    if PDF.DrawPostScript(72, 72, 0, 0, TriangleEPS) = 1 then
      PDF.SaveToFile('triangle.pdf')
    else
      Writeln('DrawPostScript failed, code ', PDF.LastErrorCode);
    // 기대: 3단위 검정 외곽선 달린 연회색 삼각형
    // 문자 그대로의 q/f/Q/S 번역은 삼각형을 채우고 외곽선을 떨어뜨립니다
  finally
    PDF.Free;
  end;
end.

PDFlibPas는 그 순간 PDF 콘텐츠 스트림이 무엇을 갖고 있든 별개로 자기 PostScript 패스를 지켜 두고, 무언가가 그리거나 클리핑할 때만 PDF 패스를 씁니다. 각 포인트는 moveto나 lineto가 돌던 때의 변환 행렬 아래에서 고정됩니다. PostScript에서와 같이, 나중의 scale이나 concat은 이미 패스 위에 있는 포인트를 결코 옮기지 않습니다. currentpoint는 저장된 포인트를 현재 행렬로 되돌려 사상하므로, 지금의 사용자 공간으로 답합니다. gsave는 패스 사슬 끝의 참조에 현재 포인트, 행렬, 폰트, 색상 공간을 더해 기록하므로, 중첩된 저장이 패스 전체를 복사할 일은 없습니다

클리핑은 두 패스 모델이 다시 갈라지는 지점이고, 임포트는 거기서도 PostScript를 따릅니다. clip과 eoclip은 PDF 쪽에는 W n을 내놓되 뒤에 흔히 오는 스트로크를 위해 논리 패스를 제자리에 두고, PostScript에서 새 빈 패스를 시작하는 rectclip은 re W n을 내놓고 그것을 칩니다. 패스 저장은 100,000 노드에서 갈리고, 재생은 공유 연산 예산을 쓰므로, 적대적 파일이 gsave와 stroke를 돌려 출력을 부풀릴 수는 없습니다

이미지 놓기: ImageMatrix는 반대쪽을 가리킵니다

EPS 이미지는 ImageMatrix를 배치로 다룰 때 뒤집혀 도착합니다. 그것은 반대쪽 사상을 묘사하기 때문입니다. PDF에서 §8.9.4는 이미지 경계를 사용자 공간의 단위 정사각형에 묶되 첫 샘플 행이 위에 옵니다. 암묵적인 이미지에서 사용자로의 행렬이 [1/w 0 0 -1/h 0 1]입니다. PostScript의 ImageMatrix는 사용자 공간을 이미지 공간으로 사상합니다. 그러므로 PDF 배치 행렬은 [w 0 0 -h 0 h]에 ImageMatrix의 역수를 곱한 것입니다. 흔한 위에서 아래로 행렬 [w 0 0 -h 0 h]의 곱은 항등입니다. 무심한 변환기가 평소 쓰임에서는 옳게 보이는 이유가 여기 있습니다. [w 0 0 h 0 0]으로 쓰인 아래에서 위로 스캔은 단위 정사각형 안의 수직 뒤집기인 [1 0 0 -1 0 1]을 주고, 회전되거나 기울어진 행렬은 특수 사례가 필요 없습니다. 유한 데이터 소스가 일찍 끝나면 PDFlibPas는 완성된 행만 그리고 원본 ImageMatrix를 지키며 실제 도착한 행들로 스케일합니다. 잘린 이미지가 올바른 스케일로 영역의 위쪽 띠를 채우는 이유입니다. 살아남은 행을 전체 높이에 걸쳐 늘이면 온전해 보이는 왜곡된 그림이 나옵니다

Delphi용 PDFlibPas EPS 임포트에서 PostScript ImageMatrix 규약에서 PDF 배치 행렬을 유도하는 다이어그램, 바른 방향과 뒤집힌 결과, 잘린 행 처리 포함
배치 행렬은 PDF 단위 정사각형 사상에 ImageMatrix 역수를 곱한 것입니다. 그래서 위에서 아래 아트워크는 바로 서로 도착하고 아래에서 위 스캔만 뒤집힙니다

imagemask는 어느 비트를 그리는가?

극성 피연산자가 true일 때 imagemask는 1 비트를 그리고, PDF에서 그것은 Decode [1 0]입니다. ISO 32000-1 §8.9.6.2는 [0 1]을 이미지 마스크의 기본값으로 정의합니다. 0 샘플이 현재 색으로 페이지를 찍고 1은 배경을 그대로 둔다는 뜻이며, [1 0]은 그것을 뒤집습니다. PostScript 불리언은 어느 샘플 값이 그리는지 말합니다. "PDF 기본값을 뒤집어라"로 읽으면 로고는 네거티브로 인쇄됩니다. 마크가 파 내진 속 빈 덩어리입니다. PDFlibPas는 true를 [1 0]으로, false를 [0 1]로 사상하고, 스텐실을 /IM true를 얹은 인라인 이미지로 내놓아 프로그램이 정한 어떤 색으로든 그리게 하며, 사전 꼴에서는 그 두 Decode 배열만 받습니다

Delphi용 PDFlibPas EPS 임포트에서 PDF Decode 배열로 사상되는 PostScript imagemask 극성: true는 1 비트를 그리고 false는 네거티브를 인쇄합니다
PostScript 불리언은 그리는 샘플 값을 이름 대고, true를 Decode [1 0]으로 사상하면 한 색 로고가 네거티브로 인쇄되는 일이 막힙니다

12비트 샘플은 더 작은 어긋남을 숨깁니다. PDF에는 12비트 이미지가 없습니다. §8.9.5.1은 컴포넌트당 1, 2, 4, 8비트와 PDF 1.5부터 16비트를 허용합니다. 각 샘플을 네 비트 왼쪽으로 밀면 아무것도 잃지 않고 16비트에 들어가지만, 최대 값은 65535가 아니라 65520이 되므로, 기본 Decode에서는 밝은 샘플마다 0.023퍼센트쯤 어둡게 나옵니다. 임포트는 밀기를 지키고 위쪽 Decode 끝점을 65535/65520으로 늘려 모든 정규화된 값을 되찾습니다. 12비트 이미지는 출력 버전을 자동으로 PDF 1.5로 올립니다. PDF/A나 PDF/X 모드에서, 혹은 출력 버전이 1.5 아래로 잠겨 있으면, 임포트는 페이지가 바뀌기 전에 오류 538로 멈춥니다

Æ는 왜 á로 나오는가?

PostScript의 로만 폰트는 프로그램이 다시 인코딩하지 않는 한 StandardEncoding을 쓰고, PDF는 그 표를 이름 댈 수 없습니다. ISO 32000-1 §9.6.6.1은 그 이름에 특별한 의미를 주지 않아 WinAnsiEncoding, MacRomanEncoding, MacExpertEncoding만 미리 정의된 선택으로 남깁니다. 부속서 D.2 표들은 코드를 그대로 WinAnsi에 넘길 때 무엇이 어긋나는지 보여 줍니다. 8진수 341(0xE1)은 StandardEncoding에서 AE이고 WinAnsi에서 aacute이므로 Æ가 á가 되고, 0xAE는 한쪽에서 fi 합자이고 다른 쪽에서 ®이며, 0x27은 StandardEncoding에서 quoteright이고 WinAnsi에서 곧은 quotesingle이라 타이포그래픽 어포스트로피가 타자기 어포스트로피로 변합니다. 프로그램이 쓰는 Standard-14 로만 폰트마다 PDFlibPas는 /Encoding 사전에 /BaseEncoding /WinAnsiEncoding과 StandardEncoding 순서로 256 코드 전부를 이름 대는 /Differences 배열을 갖춘 사적 Type 1 폰트 리소스를 쓰므로, 어떤 코드도 뷰어가 베이스 표를 채우는 방식에 의존하지 않습니다. 폭은 같은 인코딩에서 나오고, ToUnicode 맵이 복사와 검색이 Æ를 돌려주게 하며, Symbol과 ZapfDingbats는 내장 코드를 지킵니다. 임포트 전에 코드가 고른 폰트는 손대지 않고 남습니다. EPS에 맞춰 캐시된 폰트를 재인코딩하면 이미 그 폰트로 놓인 텍스트가 바뀌기 때문입니다

그 리소스들은 폰트를 포함하지 않고 이름 대는데, 포함을 요구하는 프로파일과 부딪힙니다. PDF/A, PDF/X, PDF/UA로 준비된 문서에서, 혹은 SetEmbedAllFonts(1) 뒤에는 EPS 텍스트가 오류 538로 실패하고 페이지는 원래대로 남습니다. 보관용 출력에는 텍스트가 외곽선으로 변환된 채 내보내진 EPS가 더 깨끗한 길이고, 어느 모드에서든 평범한 패스로 임포트됩니다. 이미 존재하는 파일에 포함 안 된 채 남겨진 폰트는 대신 PDF/A를 위한 누락 폰트 포함이 필요합니다. EPS 텍스트는 14 표준 폰트에 정확한 이름으로 대응됩니다. 자기 폰트 프로그램을 포함하는 아트워크는 텍스트가 외곽선이 되는 순간 깨끗하게 임포트되는데, 그것은 로컬 대체 폰트가 모든 줄을 옮기는 일도 막습니다

바운딩 박스, DSC 코멘트, 이진 EPS 파일

배치는 아트워크의 바운딩 박스를 넘기는 사각형으로 스케일하므로, 박스는 바른 곳에서 와야 합니다. PDFlibPas는 %%BoundingBox와 %%HiResBoundingBox를 진짜 DSC 코멘트 줄, 곧 줄을 시작하는 최상위 코멘트에서만 읽고, 유효한 고해상도 박스가 정수 박스를 이깁니다. 맨문자열 탐색은 그 키워드들을 리터럴 문자열과 프로시저 몸통, 이진 이미지 데이터 안에서도 건져 줍니다. EPS 파일은 셋 다 실어 다닙니다. (atend) 머리 값은 숫자들을 트레일러의 코멘트로 미루어 스캐너가 닿을 때 읽고, 박스가 전혀 없는 파일은 612 × 792로 물러나며, 빈 박스나 뒤집힌 박스는 오류 124로 실패합니다. Width와 Height는 박스를 따로 스케일합니다. 자연 크기를 지키려면 둘 다 0을 넘기고, 박스의 종횡비로 둘 다 넘기세요. 하나만 0이 아니면 아트워크가 한 축으로만 늘어납니다

이진 데이터가 진입점을 정합니다. DrawPostScript는 WideString을 받아 코드에서 지은 PostScript에는 좋지만, 날 이미지 샘플과 currentfile로 읽는 Flate 데이터, DOS EPS 래퍼는 유니코드 텍스트 여행을 못 견디는 바이트를 담습니다. ImportEPSFromFile은 파일을 날바이트로 읽고 C5 D0 D3 C6 프리뷰 헤더를 알아보고, PostScript 구간의 오프셋과 길이가 파일 안에 드는지 확인하며, TIFF나 WMF 프리뷰는 무시한 채 그 구간만 실행합니다. 두 호출은 소스를 최대 4 MiB까지 받고, Options = 0을 요구하며, 1이나 0을 돌려주고, 그림 전부 아니면 아무것도 덧붙이지 않습니다. 실패 이유는 LastErrorCode에 앉는데 읽으면 리셋되므로 한 번 잡아 두세요

uses
  SysUtils, PDFlibrary;

function PlaceEPS(PDF: TPDFlib; const FileName: string;
  Left, Top: Double): Boolean;
var
  Code: Integer;
begin
  // 0, 0: %%HiResBoundingBox나 %%BoundingBox에서 나온 자연 크기
  Result := PDF.ImportEPSFromFile(Left, Top, 0, 0, FileName) = 1;
  if Result then
    Exit;
  Code := PDF.LastErrorCode;   // 읽으면 코드가 지워지므로 한 번만 읽습니다
  case Code of
    402:
      Writeln(FileName, ': file cannot be opened');
    PDFLIB_ERROR_POSTSCRIPT_INVALID:      // 124
      Writeln(FileName, ': malformed PostScript, bad bounding box or nothing painted');
    PDFLIB_ERROR_POSTSCRIPT_UNSUPPORTED:  // 538
      Writeln(FileName, ': construct the import does not handle, or Standard-14 text in an archival profile');
    PDFLIB_ERROR_POSTSCRIPT_BUDGET:       // 539
      Writeln(FileName, ': source over 4 MiB or a work limit reached');
  else
    Writeln(FileName, ': import failed with code ', Code);
  end;
end;

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    if PlaceEPS(PDF, 'logo.eps', 40, 40) then
      PDF.SaveToFile('letterhead.pdf');
  finally
    PDF.Free;
  end;
end.

오류 538이 연산자를 이름 댈 때는 아트워크를 편집기에서 텍스트를 외곽선으로 변환한 평범한 EPS로 다시 내보내세요. 그 편이 연산자를 쫓는 것보다 보통 빠르고, 외곽선화된 아트워크는 PDFlibPas Delphi PDF Library로 지어진 PDF/A나 PDF/UA 문서에 Standard-14 폰트 충돌 없이 놓입니다