El renderer de páginas del HotPDF Delphi Component ahora avanza el texto calculando cada desplazamiento de glifo en text space, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th como lo define ISO 32000-1 §9.4.4, y luego moviendo la matriz de texto por su parte lineal con HPDFTranslateTextMatrix. El clipping se guarda por cada frame q y se restaura en Q, pero una región GDI se captura solo cuando ese frame realmente cambia el clip. Ambos arreglos aterrizaron en HotPDF 2.754.0, y ambos vinieron de páginas reales que se renderizaban con palabras colapsadas o regiones de clip que se escapaban de su Q. El primer bug es aritmética que se ve correcta hasta que un productor escribe el tamaño de fuente en la matriz. El segundo es un arreglo de corrección que casi nos cuesta el speedup de render paralelo, y cómo recuperamos la velocidad vale conocerlo si usted escribe cualquier dispositivo PDF respaldado en GDI
¿Por qué el texto se colapsa en un pegote cuando un PDF usa Tf 1?
Porque el viejo código de avance sumaba una distancia de text space directo a la componente de traslación de Tm, como si text space y user space siempre tuvieran la misma escala. Un montón de productores reales fijan el tamaño de fuente en 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 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 la pluma 0.5 puntos. Cada glifo caía un doceavo de carácter después del anterior, así que una línea de texto corrido se renderizaba como una mancha oscura en el margen izquierdo mientras el mismo archivo se veía perfecto en todos los demás visores
// Content stream de un productor 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
// Viejo avance (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 por Tfs por error
Tm.e := Tm.e + Adv; // ignora Tm.a, Tm.b, Tm.c, Tm.d
// Viejo ajuste TJ: 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 en ese bloque. El word spacing Tw se expresa en unidades de text space sin escalar, pero el viejo código lo multiplicaba por FontSize / 1000, así que bajo Tf 12 una línea justificada perdía casi todo su espacio entre palabras. La escala horizontal Th se aplicaba al ancho del glifo pero no a Tc ni a Tw, y el ajuste de kerning del TJ lo saltaba por completo. El camino sin pintado que avanza texto invisible de render mode 3, del tipo 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 cualquier cosa dibujada después de una corrida invisible arrancaba de la posición equivocada. Los bugs de estado de texto en un renderer rara vez fallan con escándalo: 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 verosímiles en la salida de la propia biblioteca y solo se rompían con archivos de otros productores
¿Cómo define ISO 32000-1 §9.4.4 el advance del glifo?
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 la escala, la rotación y el skew. Para escritura horizontal, tx iguala ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, donde w0 es el ancho del glifo en milésimas de em, Tj es el ajuste del TJ, y Th es Tz dividido por 100. El nuevo Tm es [1 0 0 1 tx 0] × Tm, que en HotPDF es el helper HPDFTranslateTextMatrix: suma X y Y a través de los coeficientes de matriz a, b, c y d en vez de escribir a e y f directo. Según §9.3.3, Tw aplica solo al código de carácter de un byte 32, así que los códigos CID multibyte jamás agarran word spacing en el camino horizontal. El mismo helper ahora maneja Td, TD, T*, los operadores ' y ", los ajustes del 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 completa
HPDFTranslateTextMatrix(Tm, Adv, 0);
// Elemento número del 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 arma la matriz completa del glifo desde CTM × Tm × rise × escala de em, incluida 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 sesgado 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 la escala horizontal no aplica a ese eje
¿Qué guarda realmente q/Q en el estado gráfico de un PDF?
ISO 32000-1 §8.4.2 lista el clipping path corriente como parte del estado gráfico, así que Q debe restaurar el clip exactamente como estaba en el q correspondiente, no solo los parámetros numéricos. HotPDF ya mantenía una pila de estado gráfico con el CTM, los colores, los parámetros de línea y el estado de texto, pero GDI guarda el clip en el device context, fuera de esa pila. Una copia del estado numérico por lo tanto restauraba todo menos el clip, y un clip instalado con W n dentro de un bloque q ... Q seguía recortando cada operación posterior de la página. Los Form XObjects agregaron una segunda ruta al mismo fallo, porque §8.10 le 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 ahora llama a CaptureClipBeforeChange y SaveDC antes de correr un form, y cuando el form termina descarta cualquier región guardada más profunda que la profundidad de entrada y llama a RestoreDC, de modo que cada HRGN guardada tiene exactamente un camino de liberación
Captura lazy de clip con THPDFSavedClipState
El arreglo que se publicó guarda un record THPDFSavedClipState por cada q, pero difiere la parte cara hasta que el frame modifica el clip por primera vez. El record carga el handle de la región, la profundidad de pila a la que pertenece, el device context del que se tomó y un flag Captured. DevPushState solo llena 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 por cambiar el clipping, o sea el pintado de paths con un W o W* pendiente, el operador n, los rellenos de pattern 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 es nuestro
Region := CreateRectRgn(0, 0, 0, 0);
if Region = 0 then RaiseLastOSError;
ClipResult := GetClipRgn(FDC, Region); // 0 significa que no hay clip
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); // Región 0 quita el clip
if FSavedClips[Index].Region <> 0 then
DeleteObject(FSavedClips[Index].Region);
Dec(FSavedClipCount);
end;
FGSStack.Pop;
end;
El costo 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 mayormente de transformaciones numéricas los hilos del renderer se pasaban el tiempo disputándose objetos de región GDI en lugar de rasterizar. El pipeline de render paralelo cayó de su ganancia esperada a aproximadamente 1.13 a 1.20 veces el throughput monohilo y reprobó la compuerta de speedup de 1.5 veces de la suite de benchmarks. Con captura lazy 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 versión y era el sospechoso obvio, pero la regresión se rastreó hasta la asignación de regiones, lo cual es un buen recordatorio de medir antes de culpar a la función 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 renderiza y sin sentido para cualquier otro objetivo. Por eso cada frame registra su device context y DevPopState se salta la restauración cuando el DC cambió, por ejemplo mientras un transparency group renderiza a su propio bitmap de capa. Que GetClipRgn devuelva cero es un resultado legítimo que significa sin clip, y restaurarlo con SelectClipRgn(FDC, 0) es lo que correctamente quita un clip que no existía en el q correspondiente. Del lado del texto, el arreglo corrige adó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 tan bueno como el fallback de anchos. Cuando pruebe regresiones de esta zona, conserve al menos un fixture con Tf 1 y un Tm escalado, 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 biblioteca
Si usted 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 simplemente deberían renderizar correctamente en 2.754.0 y posteriores. Detalles del componente, versiones soportadas de Delphi y C++Builder y licencias están en la página de producto del HotPDF Delphi PDF Component