losLab PDF Developer Library인 PDFlibPas는 문서의 모든 실수에 대해 파싱한 소수점 텍스트를 정확히 보관하고, 값이 한 번도 수정되지 않았다면 그 텍스트를 그대로 되씁니다. v3.539.19부터 SetPrecision 설정은 라이브러리가 만들거나 편집한 숫자에만 적용되므로, 평범한 load-and-save가 CalRGB /Gamma의 2.22221을 2.2222로 반올림하고 아무도 건드리지 않은 페이지의 색을 틀어 놓지 않습니다. 코드 변경은 작지만 파서에 대해 말해 주는 바는 큽니다. 디코딩한 값과 출력하는 리터럴은 서로 다른 것이고, Double 왕복은 항등 변환이 아닙니다
아무것도 바꾸지 않은 저장이 페이지 색을 틀어 놓은 이유는?
이미지가 아니라 색 공간 파라미터가 다시 포맷되고 있었기 때문입니다. 이를 드러낸 파일은 로컬 회귀 코퍼스에 있는 35페이지짜리 오피스 문서로, 머리글 이미지가 모든 페이지에 재사용됩니다. 이 파일을 읽어 그대로 다시 저장하면 이미지 스트림은 입력과 바이트 단위로 동일했고, 스트림 해시 비교는 문서가 바뀌지 않았다고 보고했습니다. 렌더링 비교는 다른 얘기를 했습니다. 35페이지 전부에서 머리글에 픽셀 차이가 나타났고, 다른 곳은 없었습니다
머리글 이미지는 CalRGB 색 공간을 통해 그려지는데, ISO 32000-1 §8.6.5.3은 이를 /WhitePoint, 선택적인 세 원소 /Gamma 배열, 선택적인 아홉 원소 /Matrix로 정의합니다. 이 배열들은 색 공간 딕셔너리 안의 평범한 숫자 객체입니다. TPDFNumeric은 각 값을 Double 하나로만 저장했고, TPDFNumeric.Output은 그 Double을 소수점 네 자리가 기본인 PDFPrecNum을 거쳐 포맷했습니다. 그래서 /Gamma는 2.22221에서 2.2222로, 행렬 원소 하나는 0.71519에서 0.7152로 바뀌었고, 렌더러는 조금 달라진 캘리브레이션으로부터 조금 다른 색을 충실히 만들어 냈습니다. 이미지 바이트는 죄가 없었고, 그 주위의 숫자가 문제였습니다. 불편한 점은 이것이 얼마나 보이지 않았는가입니다. 디코딩한 스트림 바이트를 비교해도 알 수 없습니다. 숫자가 스트림이 아니라 딕셔너리에 살기 때문입니다. 첨부 파일 페이로드를 비교해도 알 수 없습니다. 수정 레벨 글에 나오는 리비전 비교조차 정규화된 객체 본문을 지문으로 삼기 때문에, 두 리비전이 같은 값으로 해시되고 비교는 두 리비전이 동일하다고 보고합니다. 렌더링만이 이것을 잡아냈고, 그래서 코퍼스 베이스라인이 구조 검사만 믿지 않고 모든 페이지를 렌더링합니다
파싱한 값은 출력해야 할 리터럴이 아닙니다
PDF 실수는 소수점 문자열이고, ISO 32000-1 §7.3.3은 그것이 오직 소수점 문자열이라고 분명히 밝힙니다. 진법 표기도, 지수 표기도 없습니다. Annex C는 이어서 구현이 지켜야 할 정밀도로 소수부에서 대략 유효 십진 숫자 다섯 자리를 제시합니다. 기본 출력 정밀도 네 자리는 이미 그보다 낮고, 0 근처에서는 더 나빠집니다. PLDoubleToStr은 값을 스케일하고 정수로 반올림한 뒤 결과가 0이면 0을 내보내므로, 행렬 원소 -0.000012345는 자릿수를 하나 잃는 게 아니라 통째로 사라집니다
기본값을 올리는 것은 벼랑을 옮기는 일에 그칩니다. 수정은 Double이 그 숫자인 척하는 것을 그만두는 것입니다. TPDFStructure.Decode의 토크나이저가 표준 실수, 즉 소수점을 포함하고 지수 표시가 없는 토큰을 인식하면, 변환된 값과 함께 새 FOriginalText 필드에 원본 텍스트를 저장합니다. 그러면 Output이 그 텍스트를 우선하고, 우선할 것이 없을 때만 포맷으로 되돌아갑니다
// Lib/PDFlibStruct.pas — 출력 쪽 수정의 전부입니다
Function TPDFNumeric.Output: AnsiString;
Begin
If FOriginalText<> '' Then
Result:= FOriginalText
Else
Result:= PLDoubleToStr(FValue, Owner.PDFPrecNum);
End;
Procedure TPDFNumeric.SetTo(Const Value: Double);
Begin
FOriginalText:= ''; // 편집된 숫자는 새 숫자입니다
FValue:= Value;
FChanged:= True;
End;
경계 두 가지는 의도적입니다. 정수는 보존하지 않는데, 정수 포맷은 이미 무손실이기 때문입니다. 6.02E23 같은 지수 표기는 망가진 프로듀서를 위해 입력에서는 허용하지만 출력에서는 보존하지 않습니다. 그대로 되쓰면 §7.3.3이 금지하는 문법을 계속 유지하게 되기 때문입니다. 이런 값은 라이브러리가 만든 다른 숫자와 마찬가지로 포매터를 거칩니다. 토크나이저는 텍스트를 저장하기 전에 늘 하던 최소한의 보정도 적용해서, .5처럼 점으로 시작하는 리터럴은 0.5로, 5.처럼 점으로 끝나는 리터럴은 5.0으로 유지합니다. 둘 다 모든 리더에게 같은 숫자이고 훨씬 폭넓게 받아들여집니다
v3.539.19 이후 SetPrecision이 보장하는 것은?
TPDFlib.SetPrecision은 이제 라이브러리 자체가 만들어 내는 숫자의 소수 자릿수를 제어합니다. 페인터를 통해 그려지는 값, NewNumeric처럼 Double에서 만들어지는 숫자, 그리고 이후 SetTo로 편집된 모든 파싱 값입니다. 객체 API로 디코딩한 텍스트, 예를 들어 SetObjectFromString에 넘긴 리터럴도 같은 토크나이저를 거쳐 같은 방식으로 보존됩니다. 한 번도 수정되지 않은 파싱 소수는 설정과 무관하게 입력 정밀도를 유지하고, 로드 후에 설정을 바꿔도 소급해서 영향을 주지 않습니다. SetPrecision 레퍼런스 항목도 같은 릴리스에서 정확히 이 내용으로 갱신했습니다. 예전 문구가 이 설정이 파일 안의 모든 숫자에 적용되는 것처럼 암시했기 때문입니다
비우기는 Changed 플래그에서 파생되는 것이 아니라 SetTo에서 일어나고, 이 구분이 중요합니다. 저장 파이프라인은 객체가 기록된 뒤 Changed를 초기화하므로, "변경되지 않았으면 원본 텍스트를 출력" 같은 검사는 같은 세션에서 편집, 저장, 재편집된 값에 대해 낡은 텍스트를 출력하기 시작했을 것입니다. 원본 텍스트를 대입 자체에 묶어 두면 둘이 어긋날 수 없습니다. 회귀 테스트는 원본 파일의 값으로 이 동작 하나하나를 고정합니다
uses
PDFlibStruct;
var
Structure: TPDFStructure;
Values: TPDFArray;
Number: TPDFNumeric;
begin
Structure := TPDFStructure.Create;
try
Structure.PDFPrecNum := 4;
Values := TPDFArray(Structure.Decode('[2.22221 0.71519 -0.000012345 1 0.12567]'));
// 편집하지 않은 입력은 그대로 살아남으며, 네 자리 포맷이면
// 0으로 붕괴됐을 값까지 포함합니다
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// 편집하면 원본 텍스트를 버리고 PDFPrecNum을 따릅니다
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// 나중에 정밀도를 낮춰도 편집하지 않은 입력에는 닿지 않습니다
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
콘텐츠 모델은 왜 여전히 숫자를 정규화할까요?
TPDFContentProgram이 정규 숫자 오퍼랜드를 약속하고, 그 약속이 콘텐츠 스트림 안에서 텍스트를 그대로 보존하는 것보다 가치 있기 때문입니다. 그래픽 상태 트래커가 올라가 있는 것과 같은 편집 가능한 콘텐츠 모델은 NormalizeContentStreams, 옵티마이저, Emit이 임의의 입력에서 안정적이고 비교 가능한 출력을 만들도록 존재합니다. 파싱한 오퍼랜드가 원본 텍스트를 모델로 가져가면 0.50000 0 0 RG 같은 연산자 시퀀스가 0.5 0 0 RG와 다르게 출력되고, 모든 하류 비교가 프로듀서의 포맷 습관에 따라 흔들렸을 것입니다
그래서 모델은 두 진입점에서 원본 텍스트를 벗겨 냅니다. NormalizeContentNumbers는 파서가 각 오퍼랜드를 밀어 넣을 때, 그리고 호출자가 넘긴 소스가 디코딩될 때 SetOperand 안에서 다시 실행되며, 배열과 딕셔너리를 재귀하므로 dash 패턴, TJ 배열, marked content의 속성 딕셔너리까지 모두 포함됩니다. 각 숫자에 SetTo(AsDouble)을 호출하는 것만으로 충분합니다. 텍스트를 비우는 연산이 바로 그것이기 때문입니다. 원시 인라인 이미지 데이터는 언제나 그랬듯 손대지 않습니다
// Lib/PDFlibContentModel.pas — 콘텐츠 모델은 계약을 유지합니다
Procedure NormalizeContentNumbers(Obj: TPDFObject);
Var
K: Integer;
Begin
If Obj is TPDFNumeric Then
TPDFNumeric(Obj).SetTo(TPDFNumeric(Obj).AsDouble)
Else If Obj is TPDFArray Then
For K:= 0 To TPDFArray(Obj).Count- 1 Do
NormalizeContentNumbers(TPDFArray(Obj).Item[K])
Else If Obj is TPDFDictionary Then
For K:= 0 To TPDFDictionary(Obj).Count- 1 Do
NormalizeContentNumbers(TPDFDictionary(Obj).Entry[K].Value);
End;
그래서 호출자를 위한 실용 규칙은 단순합니다. 평범한 LoadFromFile과 SaveToFile 조합은 손대지 않은 콘텐츠 스트림과 딕셔너리 숫자를 그대로 남깁니다. NormalizeContentStreams를 거친 페이지나 콘텐츠 모델을 통한 편집은 설계대로 정규 형태로 나오고, 나머지 문서는 여전히 보존됩니다. 이 둘은 서로 다른 요청이고, 이제 서로 다른 일을 합니다
그 대가와 보장이 멈추는 지점
이제 모든 TPDFNumeric이 AnsiString 참조를 하나씩 더 들고 다니고, 파싱한 모든 소수가 객체의 수명 동안 원본 텍스트를 살려 둡니다. 실수가 수백만 개인 문서에서는 이만저만한 메모리가 아니며, 이는 휙 넘길 일이 아니라 대용량 문서 측정 항목에 들어가야 할 값입니다. 보장 범위도 숫자가 속한 문서로 한정됩니다. 문서 사이에 객체를 복사하거나 객체 API로 값을 다시 만들면 새 숫자가 만들어지고, 그 숫자는 다른 새 숫자와 마찬가지로 출력 정밀도를 따릅니다. 이 릴리스가 무엇을 주장하고 무엇을 주장하지 않는지는 정확히 짚을 가치가 있습니다. 손대지 않은 문서의 load-and-save는 이제 렌더러가 실제로 소비하는 캘리브레이션 숫자를 보존하고, 그것이 코퍼스 베이스라인이 검사하는 속성입니다. 바이트 단위로 동일한 출력을 주장하지는 않습니다. 그것은 객체 번호 매기기, 스트림 압축, 그리고 결정적 PDF ID 글에서 다루는 트레일러 식별자에도 달려 있습니다. 다른 소프트웨어가 만든 파일의 반올림 차이를 지문 비교가 보게 만들어 주지도 않습니다. 그런 파일들은 여전히 정규화된 본문을 해시하기 때문입니다. 교훈은 CalRGB를 훨씬 넘어섭니다. 파서가 변환된 값만 남기면 모든 저장이 편집이 되고, 그것을 알아채는 유일한 방법은 렌더링 결과를 보는 것입니다. 숫자 처리와 SetPrecision 의미는 losLab PDF Developer Library 제품 페이지에 문서화되어 있습니다