El renderer de páginas del HotPDF Delphi Component avanza ahora el texto calculando el desplazamiento de cada glifo en text space, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th como lo define ISO 32000-1 §9.4.4, y moviendo después la matriz de texto por su parte lineal con HPDFTranslateTextMatrix. El recorte se guarda por frame de q y se restaura en Q, pero una región GDI solo se captura cuando ese frame cambia de verdad el clip. Ambos fixes aterrizaron en HotPDF 2.754.0, y los dos vinieron de páginas reales que renderizaban con palabras aplastadas o regiones de clip que se escapaban de su Q. El primer bug es aritmética que parece correcta hasta que un producer escribe el tamaño de fuente en la matriz. El segundo es un fix de corrección que casi nos cuesta el speedup de render paralelo, y la forma de recuperar la velocidad merece conocerse si usted escribe cualquier dispositivo PDF respaldado por GDI
¿Por qué el texto se apelotona cuando un PDF usa Tf 1?
Porque el viejo código de avance sumaba una distancia de text space directamente a la componente de traslación de Tm, como si text space y user space tuvieran siempre la misma escala. Muchos producers reales fijan el tamaño de fuente a 1 con Tf y llevan el tamaño real en la matriz de texto. Con /F1 1 Tf y 12 0 0 12 72 700 Tm, un glifo de 500 unidades de ancho avanza 0.5 en text space, que son 6 puntos en la página una vez que Tm lo escala. El viejo renderer ejecutaba Tm.e := Tm.e + Adv y movía el lápiz 0.5 puntos. Cada glifo caía un doceavo de carácter después del anterior, así que una línea de texto de cuerpo se renderizaba como una mancha oscura en el margen izquierdo mientras el mismo archivo se veía perfecto en cualquier otro visor
// Content stream de un producer que codifica el tamaño en Tm, no en Tf:
// BT
// /F1 1 Tf
// 12 0 0 12 72 700 Tm
// [(Hel) 30 (lo) -250 (world)] TJ
// ET
// Advance viejo (simplificado): distancia sumada a Tm.e como si fuera user space
Adv := W * FontSize / 1000; // 0.5 para un glifo de 500 unidades
if (HorizScale <> 0) and (HorizScale <> 100) then
Adv := Adv * HorizScale / 100; // Th solo sobre el ancho
Adv := Adv + CharSpace; // Tc sin escalar por Th
if Code = 32 then
Adv := Adv + WordSpace * FontSize / 1000; // Tw escalado erróneamente por Tfs
Tm.e := Tm.e + Adv; // ignora Tm.a, Tm.b, Tm.c, Tm.d
// Ajuste TJ viejo: sin Th, y de nuevo solo Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;
El atajo de Tm.e no era el único defecto de ese bloque. El word spacing Tw se expresa en unidades de text space sin escalar, y aun así el código viejo lo multiplicaba por FontSize / 1000, así que bajo Tf 12 una línea justificada perdía casi todo su hueco entre palabras. El horizontal scaling Th se aplicaba al ancho del glifo pero no a Tc o Tw, y el ajuste de kerning TJ lo saltaba por completo. El camino que no pinta, el que avanza el texto invisible del modo de render 3, del que usan las capas de texto OCR, y el texto dentro de optional content oculto llevaban una copia privada de la misma aritmética, así que todo lo dibujado después de una tirada invisible arrancaba de la posición equivocada. Los bugs de estado de texto en un renderer rara vez fallan ruidosamente: como los bugs de índice de operando y nombre de recurso que una vez pusieron a cero Tc, Tw y Tz sin un solo error, estos producían páginas plausibles en la salida de la propia librería y solo se rompían con archivos de otros producers
¿Cómo define el advance del glifo ISO 32000-1 §9.4.4?
ISO 32000-1 §9.4.4 define el advance enteramente en text space y lo aplica a la matriz de texto como una matriz de traslación, así que la respuesta es calcular tx primero y dejar que Tm haga el escalado, la rotación y el skew. Para escritura horizontal, tx es ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, donde w0 es el ancho del glifo en milésimas de em, Tj es el ajuste TJ, y Th es Tz dividido por 100. La nueva Tm es [1 0 0 1 tx 0] × Tm, que en HotPDF es el helper HPDFTranslateTextMatrix: suma X e Y a través de los coeficientes a, b, c y d de la matriz en vez de escribir en e y f directamente. Según §9.3.3, Tw solo aplica al código de carácter de un solo byte 32, así que los códigos CID multibyte nunca recogen word spacing en el camino horizontal. El mismo helper maneja ahora Td, TD, T*, los operadores ' y ", los ajustes TJ y el camino de texto oculto, lo que significa que una sola función es dueña de la regla
procedure HPDFTranslateTextMatrix(var Matrix: THPDFAffineMatrix; X, Y: Double);
begin
Matrix.e := Matrix.e + Matrix.a * X + Matrix.c * Y;
Matrix.f := Matrix.f + Matrix.b * X + Matrix.d * Y;
end;
// Advance de glifo horizontal, ISO 32000-1 9.4.4
W := HPDFFontCharWidth(F, Code);
Adv := W * State.Text.FontSize / 1000 + State.Text.CharSpace;
if (Code = 32) and not F.CID2Byte then
Adv := Adv + State.Text.WordSpace; // Tw en text space, sin escalar
Adv := Adv * State.Text.HorizScale / 100; // Th aplica a la suma entera
HPDFTranslateTextMatrix(Tm, Adv, 0);
// Elemento numérico TJ: mismo espacio, mismo Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);
La colocación de glifos tuvo que seguir la misma lógica. Cuando no hay contorno incrustado disponible y el renderer cae al TextOutW de GDI, ahora construye la matriz completa del glifo a partir de CTM × Tm × rise × escala em, incluido Th, y la instala con SetWorldTransform en modo GM_ADVANCED dentro de un par SaveDC / RestoreDC. La fuente GDI se crea a una altura fija de 1000 unidades y la transformación hace el dimensionado, así que el texto rotado y oblicuo conserva su orientación en lugar de dibujarse derecho en un punto de origen transformado. El modo de escritura vertical es la única asimetría deliberada: una fuente WMode 1 avanza hacia abajo por el eje y según su métrica vertical, y el horizontal scaling no aplica a ese eje
¿Qué guarda en realidad q/Q en un estado gráfico PDF?
ISO 32000-1 §8.4.2 lista el camino de recorte actual como parte del estado gráfico, así que Q debe restaurar el clip exactamente como estaba en el q que le corresponde, y no solo los parámetros numéricos. HotPDF ya mantenía una pila de estado gráfico con la CTM, los colores, los parámetros de línea y el estado de texto, pero GDI guarda el clip en el contexto de dispositivo, fuera de esa pila. Una copia del estado numérico por tanto restauraba todo salvo el clip, y un clip instalado con W n dentro de un bloque q ... Q siguió recortando cada operación posterior de la página. Los Form XObjects añadían una segunda ruta al mismo fallo, porque §8.10 da a un form un save y restore implícitos alrededor de su contenido, y el contenido de forms reales a veces deja sus propios operadores q desbalanceados aunque la especificación exige que se emparejen. El renderer llama ahora a CaptureClipBeforeChange y SaveDC antes de ejecutar un form, y cuando el form acaba descarta las regiones guardadas más profundas que la profundidad de entrada y llama a RestoreDC, de modo que cada HRGN guardado tiene exactamente una ruta de liberación
Captura perezosa de clip con THPDFSavedClipState
El fix que se envió guarda un record THPDFSavedClipState por q, pero difiere la parte cara hasta que el frame modifica por primera vez el clip. El record guarda el handle de región, la profundidad de pila a la que pertenece, el contexto de dispositivo del que se tomó y un flag Captured. DevPushState solo rellena la profundidad y el DC y hace crecer el array de frames duplicando desde 16, así que un content stream lleno de q 1 0 0 1 x y cm ... Q no asigna ningún objeto GDI. Los operadores que están a punto de cambiar el recorte, es decir el pintado de camino con un W o W* pendiente, el operador n, los rellenos de patrón y la entrada a forms, llaman primero a CaptureClipBeforeChange
procedure THPDFPageRenderer.CaptureClipBeforeChange;
var
Index, ClipResult: Integer;
Region: HRGN;
begin
ClearSavedClipRegions(FGSStack.Count);
Index := FSavedClipCount - 1;
if (Index < 0) or FSavedClips[Index].Captured or
(FSavedClips[Index].StackDepth <> FGSStack.Count) or
(FSavedClips[Index].DC <> FDC) then Exit; // ya guardado, o no nuestro
Region := CreateRectRgn(0, 0, 0, 0);
if Region = 0 then RaiseLastOSError;
ClipResult := GetClipRgn(FDC, Region); // 0 significa que no hay clip ninguno
if ClipResult <= 0 then
begin
DeleteObject(Region);
Region := 0;
if ClipResult < 0 then RaiseLastOSError;
end;
FSavedClips[Index].Region := Region;
FSavedClips[Index].Captured := True;
end;
procedure THPDFPageRenderer.DevPopState;
var
Index: Integer;
begin
ClearSavedClipRegions(FGSStack.Count);
Index := FSavedClipCount - 1;
if (Index >= 0) and (FSavedClips[Index].StackDepth = FGSStack.Count) then
begin
if FSavedClips[Index].Captured and (FSavedClips[Index].DC = FDC) then
SelectClipRgn(FDC, FSavedClips[Index].Region); // Region 0 elimina el clip
if FSavedClips[Index].Region <> 0 then
DeleteObject(FSavedClips[Index].Region);
Dec(FSavedClipCount);
end;
FGSStack.Pop;
end;
El coste medido de la versión ansiosa es la razón de que exista este diseño. La primera implementación correcta creaba y leía una región GDI en cada q, y en páginas hechas sobre todo de transformaciones numéricas los hilos del renderer se pasaban el tiempo compitiendo por objetos de región GDI en lugar de rasterizar. El pipeline de render paralelo cayó de su ganancia esperada a unas 1,13 a 1,20 veces el throughput monohilo y suspendió la compuerta de speedup de 1,5 veces de la suite de benchmarks. Con captura perezosa y capacidad de frames reutilizada, el mismo benchmark vuelve a pasar la compuerta original de 1,5 veces. El antialiasing de glifos TrueType pequeños aterrizó en la misma release y era el sospechoso obvio, pero la regresión se rastreó hasta la asignación de regiones, que es un buen recordatorio para medir antes de culpar a la feature más nueva
¿Dónde están los límites de este enfoque?
El clip guardado es una región GDI en píxeles de dispositivo, así que es exacto para el bitmap que se está renderizando y carente de sentido para cualquier otro destino. Por eso cada frame registra su contexto de dispositivo y DevPopState se salta la restauración cuando el DC cambió, por ejemplo mientras un grupo de transparencia renderiza a su propio bitmap de capa. Que GetClipRgn devuelva cero es un resultado legítimo que significa que no hay clip, y restaurarlo con SelectClipRgn(FDC, 0) es lo que correctamente elimina un clip que no existía en el q correspondiente. Del lado del texto, el fix corrige a dónde va cada glifo, pero no inventa anchos: si una fuente omite su array /Widths y el programa incrustado no está disponible, el advance sigue siendo solo tan bueno como el fallback de anchos. Cuando pruebe esta zona en regresión, conserve al menos un fixture con Tf 1 y una Tm escalada, uno con Tz y Tw distintos de cero, y uno con un clip dentro de q ... Q seguido de contenido fuera, porque ninguno de esos aparece en documentos generados por la propia librería
Si maneja el renderer desde código de aplicación, nada cambia en el patrón de llamada descrito en renderizar una página PDF a un bitmap, y las páginas que antes mostraban líneas emborronadas o contenido recortado deberían sencillamente renderizar bien en 2.754.0 y posteriores. Los detalles del componente, las versiones de Delphi y C++Builder soportadas y las licencias están en la página de producto del componente PDF HotPDF para Delphi