Delphi용 losLab PDF Developer Library인 PDFlibPas는 콘텐츠 스트림에 넣는 모든 숫자를, Windows 국가 설정이 뭐라 하든 점 소수 구분자와 지수 없이 기록합니다. v3.539.26부터 AddPageMatrix, ScalePage, DeskewPage, RedactRegion, text-to-path 출력, 재색칠은 피연산자를 PLDoubleToStrConst로 포맷하고, v3.539.33부터 그 숫자들을 되읽는 파서들은 시스템 로캘 대신 PLTryStrToFloatInvariant를 씁니다. 독일어, 프랑스어, 브라질 시스템에서 같은 코드가 이제 미국 시스템과 같은 바이트를 만들어 냅니다. 파일 포맷이 견딜 수 있는 유일한 동작입니다
콤마 소수 로캘이 왜 오류 없이 PDF를 망가뜨릴까?
콤마 소수 로캘은 조용히 PDF를 망가뜨립니다. 콤마는 PDF 문법에서 숫자 문자가 아니므로 손상이 잘못된 의미의 유효한 토큰으로 읽히거든요. 수정 전 PLFloatToStr는 벌거벗은 FloatToStr 호출에 불과했고, FloatToStr는 FormatSettings.DecimalSeparator를 따릅니다. 콤마 구분자에서 AddPageMatrix(0.5, 0.5, 0, 0)는 0,5 0 0 0,5 0 0 cm을 썼습니다. ISO 32000-1 §7.3.3은 숫자 안에 숫자, 마침표 하나, 선행 부호만 허용하므로, 콘텐츠 파서는 그 줄을 숫자 0 뒤에 모르는 토큰 ,5가 붙은 것으로 읽고 cm 연산자는 엉뚱한 피연산자를 받습니다. 아무것도 raise하지 않고, 아무것도 기록하지 않습니다. 페이지는 그저 어긋난 변환 행렬로 렌더링될 뿐이고, 어긋난 드로잉에서 거슬러 올라가 로캘 설정을 찾아내는 것은 끔찍한 오후가 됩니다
둘째 결함은 첫째 뒤에 숨어 있습니다. FloatToStr는 ffGeneral 포맷을 쓰는데, 이것은 크기가 1E-4 아래로 떨어지는 순간 지수 표기로 전환하므로 아주 작은 오프셋이 1E-5로 나왔습니다. 같은 §7.3.3은 PDF가 지수 형태를 지원하지 않는다고 밝히므로, 미국 로캘 기계조차 충분히 작은 값만 주어지면 무효한 피연산자를 쓸 수 있었다는 뜻입니다. 이 릴리스의 회귀 테스트는 두 실패 모양을 모두 못 박습니다. 구분자를 콤마로 바꾸고 API를 호출한 다음, 결과 콘텐츠에서 콤마나 지수를 담은 토큰을 훑습니다
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
OldSeparator: Char;
begin
Lib := TPDFlib.Create;
try
Lib.SetPageDimensions(300, 200);
Lib.DrawBox(10, 10, 20, 20, 1);
OldSeparator := FormatSettings.DecimalSeparator;
try
FormatSettings.DecimalSeparator := ','; // de-DE 데스크톱 흉내
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 이상이 기록: 0.5 0 0 0.25 0.00001 12.75 cm
// 구버전 빌드가 기록: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
두 종류의 숫자, 두 계열의 헬퍼
PDFlibPas의 수정은 엄격한 분리입니다. 사람에게 보이는 숫자는 로캘을 따를 수 있고, 기계를 위해 쓰이는 숫자는 절대 따르지 않습니다. PLFloatToStr와 PLStrToFloat는 유저 대면 텍스트를 위해 PDFlibExtra.pas에 남고, 그 선언에는 이제 정확히 그 뜻의 주석이 붙습니다. PDF 문법이 되는 모든 것은 일에 맞춰 고정된 소수 자릿수를 정한 PLDoubleToStrConst를 통과합니다. 행렬은 여섯, 좌표와 TJ 조정은 넷, 색과 FDF 사각형은 셋입니다. v3.539.26 감리는 원래 버그 리포트가 암시한 것보다 많은 호출 지점을 건드렸습니다:
AddPageMatrix,ScalePage와DeskewPage. 셋 모두 기존 페이지 콘텐츠 앞에cm을 덧붙입니다Tm리셋,TJ어드밴스,cm변환을 내보내는 페이지 엘리먼트 빌더들- text-to-path 변환기의 글리프 배치 행렬과 아웃라인 점들
RedactRegion이 앞에 붙이는 검정 채움 상자, FDF 내보내기의/Rect값, 재색칠이 쓰는 피연산자들
PLDoubleToStrConst는 FloatToStrF를 감싼 것이 아니라 직접 만든 포매터이고, 여기서 중요한 성질은 셋입니다. 항상 마침표를 쓰고 뒤따르는 0을 깎아 내므로 0.5는 0.500000이 아니라 0.5로 남습니다. 유한한 입력에는 지수를 쓰지 않습니다. 그리고 요청된 정밀도보다 작은 0 아닌 값은 0으로 뭉개지는 대신 유효 숫자를 유지하므로, PLDoubleToStrConst(1E-9, 6)는 0.000000001을 반환합니다. 대략 5E-16 아래의 값만 0이 됩니다. 마지막 규칙이 존재하는 이유는, 아주 작은 스케일 팩터를 0으로 반올림하면 유효한 행렬이 특이(singular) 행렬로 변하는데, 그건 지금 고치는 버그보다 더 나쁜 버그이기 때문입니다
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// 기계 출력: 점 소수, 지수 없음, 뒤따르는 0 제거
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // 유효 숫자 4개 유지
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// 기계 입력: EConvertError 대신 부드러운 실패
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // 콘텐츠 숫자는 콤마를 쓰지 않음
end;
파싱 쪽이 쓰는 쪽보다 더 위험한 이유는?
파싱 쪽이 더 위험한 이유는, 로캘 종속 파서는 틀린 숫자를 만드는 게 아니라 throw하기 때문입니다. PLStrToFloat는 StrToFloat를 부르는데, 텍스트가 시스템 구분자와 맞지 않으면 EConvertError를 raise합니다. 콤마 소수 시스템에서 그것은 RecolorPage가 평범한 0.5 g 연산자를 만나는 순간 중단된다는 뜻이었으니, 별난 페이지가 아니라 모든 실전 페이지가 실패했습니다. RenderPageRegionToFile는 자기 문서에 적힌 클립 포맷 "10.5,20.5,50.5,40.5"를 거부했고, SVG 길이 어트리뷰트, SVG 내보내기 색, 어노테이션 버텍스 목록, output intent 솔리디티 값은 거부되거나 조용히 디폴트로 바뀌었습니다. 개발자 기계에서 완벽히 동작하다 뮌헨의 첫 고객에게서 실패하는 라이브러리는, 우연히 동작하는 Delphi 코드에 관한 글의 사례들처럼, 테스트된 장소 때문에만 올바르게 보이는 코드의 전형입니다
v3.539.33은 모든 StrToFloat과 TryStrToFloat 호출을 입력이 어디서 오는지로 분류했습니다. 콘텐츠 스트림 피연산자, SVG 어트리뷰트, 페인터 색 문자열, 콤마로 구분된 클립과 버텍스 목록은 모두 고정된 점 문법을 갖으므로, 이제 PLTryStrToFloatInvariant를 통과합니다. 이것은 텍스트를 트림하고 PLInvariantFormatSettings로 파싱하며, 비었거나 형태가 잘못되었거나 유한하지 않은 입력에 raise 대신 False를 반환합니다. 콤마로 구분된 목록에는 타협의 여지가 없습니다. 콤마가 목록 구분자이면서 동시에 소수점일 수는 없으니까요. 같은 패스는 범위를 벗어나는 쓰기도 하나 고쳤습니다. RenderPageRegionToFile는 예전에 네 엘리먼트 버퍼를 넘어 다섯째 클립 값을 저장하곤 했습니다. PDF를 하나의 색 공간으로 변환하기 가이드에서 기술한 재색칠 파이프라인의 실용적 결과는, RecolorPage와 RecolorDocument가 콤마 소수 시스템에서 더 이상 중단하지 않는다는 것입니다. 호출자가 CheckDocumentPolicy에 입력하는 규칙 값은 대신 순한 헬퍼를 쓰는 유일한 파싱 케이스인데, 이유는 다음 절에서 설명합니다
왕복의 한쪽 끝만 고치면 무슨 일이 벌어질까?
로캘 왕복의 한쪽 끝만 고치면 동작하던 코드가 깨집니다. 그래서 v3.539.32의 구조 어트리뷰트 변경은 기록자와 읽기를 함께 움직였습니다. SetStructElem* 래퍼들은 숫자를 문자열로 중계합니다. SetStructElemBBox는 네 값을 하나의 문자열로 포맷해 AddTagAttribute로 저장하고, /A 기록자는 나중에 그 문자열을 파싱해 숫자가 될지, 배열이 될지, 이름이 될지 판단합니다. 양쪽 끝 모두 시스템 로캘을 썼으므로 콤마 소수 시스템에서의 왕복은 자기 일관적이었습니다. 버그는 호출자가 문서를 따라 "0.5"를 AddTagAttribute에 넘겼을 때에만 드러났습니다. 읽기가 파싱하지 못해 PDF 이름 /0.5를 내보낸 것이죠. PDF/VCR 플레이스홀더는 거울상 문제가 있었습니다. 라이브러리가 GTS_BBox를 점으로 생성한 다음 저장 전에 로캘로 검증했으니까요
기록자만 점으로 바꾸는 것은 아무것도 안 하는 것보다 나빴을 것입니다. 모든 SetStructElem* 값이 로캘 종속 읽기에서 실패해 이름으로 퇴화했을 테니까요. 그래서 기록자는 이제 PLDoubleToStrConst(v, 6)를 쓰고, 읽기는 점 형태를 먼저 시도한 뒤 시스템 로캘로 폴백하는 새 PLTryStrToFloatLenient를 씁니다. 예전에 "1,25"를 넘기던 콤마 로캘 호출자는 여전히 숫자 1.25를 받습니다. 트레이드오프는 의도적이고 문서화되어 있습니다. 독일어 시스템에서 "1.500"은 예전에 StrToFloat가 천 단위 구분자를 거부하므로 이름이 되었지만 이제 1.5로 읽히고, literal NAN과 INF 문자열은 더 이상 숫자로 받아들여지지 않습니다
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // 콤마 소수 호출자
Lib := TPDFlib.Create;
try
Lib.BeginTag('Figure', 'Sales chart', '');
Lib.SetStructElemBBox(10.5, 20.25, 200.5, 100.75); // /BBox [ 10.5 20.25 200.5 100.75 ]
Lib.AddTagAttribute('Layout', 'SpaceAfter', '0.5'); // /SpaceAfter 0.5, 예전에는 /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // 여전히 /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
NaN과 무한대가 막히는 곳
AddPageMatrix, ScalePage, RedactRegion는 이제 NaN과 무한대 인자를 앞에서 거부하고 0을 반환합니다. 그것들을 표현할 수 있는 PDF 숫자는 없으니까요. ScalePage는 이미 0 이하의 팩터를 거부했지만 NaN은 <= 0 검사를 통과하므로, NaN 스케일은 예전에 포매터까지 여행했습니다. v3.539.26에서 그 포매터는 NaN에 여전히 Round를 불렀고, 이는 x87 유닛이 무효 연산을 마스크하지 않는 Win32에서 EInvalidOp를 raise합니다. v3.539.31은 최후의 방어선으로 PLDoubleToStrConst가 NaN에 0을 쓰게 했지만, 행렬 안의 0은 특이 변환이므로 API 수준 검사가 여전히 진짜 수정입니다. 두 경계는 의도적으로 그대로입니다. 메타파일 상태 문자열은 한 프로세스 안에서 로캘로 쓰고 읽으며 밖으로 나가지 않으므로 그대로 두었습니다. 그리고 페이지 엘리먼트 경로로 1E-5를 포맷하는 테스트는 레이어가 다시 쓰이기 전에 콘텐츠를 읽어야 합니다. 문서 정밀도로 피연산자를 재출력하면 그 값이 정당하게 0이 되기 때문입니다
애플리케이션이 점 소수 세상 밖의 고객에게 나간다면 가장 안전한 습관은 PDFlibPas 테스트 스위트가 이제 쓰는 것입니다. PDF를 만드는 경로를 FormatSettings.DecimalSeparator를 콤마로 세팅해 한 번 돌리고, 출력에서 콤마와 지수를 훑는 것입니다. 파싱된 소수 정밀도 보존에 관한 글이 같은 이야기의 나머지 절반, 기존 파일에서 읽은 숫자가 저장 시 정확한 텍스트를 유지하는 방법을 다룹니다. 다운로드, 전체 API 레퍼런스, 트라이얼 빌드는 PDFlibPas Delphi PDF library 제품 페이지에 있습니다