El port parecía terminado el viernes. Un visor PDF de Delphi, movido a Lazarus en dos días: algunos cambios de nombres de unidades, unos cuantos bloques {$IFDEF FPC}, y el proyecto compilaba sin errores. La prueba del lunes contó otra historia: el cuadro de búsqueda no devolvía resultados para palabras claramente visibles en la página, y un nombre de archivo con caracteres acentuados se negaba a abrir. Nada en la salida del compilador había insinuado ninguna de las fallas, porque ambas eran problemas de codificación de cadenas, y los desajustes de codificación son invisibles hasta que fluyen datos reales. Portar un visor construido sobre PDFium Component, que incluye ediciones VCL y LCL desde una sola fuente, es en gran parte mecánico; las partes que no son mecánicas son exactamente las que cubre este artículo.
El mismo Pascal, distintas cargas útiles de string
El string nativo de Delphi es UTF-16 desde 2009. Lazarus y Free Pascal usan UTF-8 de forma predeterminada en aplicaciones LCL. Las APIs del componente orientadas a texto hablan UTF-16 mediante el tipo WString, que la compilación FPC aliasa a WideString; por lo tanto, cada frontera donde el texto cruza entre su UI LCL y el motor PDF es un punto de conversión.
Las conversiones son automáticas en asignaciones sencillas, pero dos hábitos evitan la clase de errores del lunes por la mañana. Pasen texto directamente sin manipulación a nivel de bytes: el código que corta un término de búsqueda por desplazamiento de byte funciona en Delphi, donde un Char es una unidad UTF-16, y corrompe UTF-8 multibyte en LCL. Y prueben con datos no ASCII desde el primer día: un nombre de archivo en alemán, un término de búsqueda con una raya, porque los datos de prueba solo ASCII vuelven invisible cada error de codificación.
Una capa condicional pequeña en lugar de un fork
La tentación después de la primera docena de IFDEFs es bifurcar la base de código por IDE. Resístanla: las diferencias caben en un bloque compartido de declaraciones, y un fork duplica cada corrección futura. La capa condicional se mantiene así de pequeña:
{$IFDEF FPC}
uses
LCLType, Forms, Graphics, Controls;
type
WString = WideString; // component text APIs are UTF-16
TBytes = array of Byte;
{$ELSE}
uses
Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}
Todo debajo de este bloque — manejo de documentos, navegación de páginas, llamadas de renderizado — compila igual en ambos IDEs, porque TPdf y TPdfView exponen la misma superficie en las ediciones VCL y LCL. La disciplina que mantiene eso cierto es estructural: la lógica PDF compartida vive en unidades sin diálogos o paneles específicos de framework, y cualquier cosa que realmente difiera (diálogos de impresión, selectores de archivo con convenciones de plataforma) vive detrás de una interfaz delgada implementada una vez por framework. El bloque IFDEF anterior también es el lugar correcto para cualquier divergencia futura de plataforma, lo que mantiene al resto de la base de código libre de condiciones de compilador dispersas.
Construyan el formulario en código, no en dos diseñadores
El streaming de formularios es donde los proyectos de doble IDE se pudren en silencio: un .dfm y un .lfm que describen "el mismo" formulario se desvían propiedad por propiedad hasta que las dos compilaciones se comportan distinto por razones que nadie puede diffear. Para el núcleo del visor, la construcción en tiempo de ejecución evita todo el problema: una secuencia de constructor, versionada como código ordinario:
procedure TViewerForm.FormCreate(Sender: TObject);
begin
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.FitMode := pfmFitWidth;
if ParamCount > 0 then
begin
Pdf.FileName := ParamStr(1);
Pdf.Active := True; // opens the document; PageCount valid after this
end;
end;
La secuencia importa menos que el enlace: PdfView.Pdf := Pdf une el control visual al componente de documento, después de lo cual la navegación de páginas mediante PageNumber y el zoom mediante FitMode se comportan igual bajo VCL y LCL. Un comportamiento entre frameworks que conviene saber antes de que los usuarios lo encuentren: asignar Zoom manualmente vuelve a poner FitMode en pfmNone en ambos frameworks. Si su barra de herramientas ofrece "ajustar al ancho" como preferencia persistente, restauren el modo de ajuste después de cualquier cambio programático de zoom, o la preferencia dejará de ser persistente en silencio.
El binario del que el IDE nunca advirtió
El componente envuelve el motor PDFium, que se distribuye como binario de plataforma; y la carga de binarios es de donde vienen los reportes de "funciona en el IDE, falla desde el acceso directo instalado". Tres reglas cubren casi todos. La arquitectura debe coincidir exactamente: un ejecutable de 32 bits no puede cargar una biblioteca pdfium de 64 bits, y el mensaje de falla que produce el sistema operativo ("módulo no encontrado" en algunas versiones de Windows) engaña activamente, porque el archivo está ahí. Resuelvan la ruta de la biblioteca relativa al ejecutable, nunca al directorio de trabajo: los lanzamientos desde el IDE y desde el shell difieren precisamente en su directorio de trabajo. Y muestren los errores de carga antes de abrir el primer documento, con un mensaje que nombre la ruta y arquitectura esperadas; un ticket que dice "binario PDFium de 64 bits faltante en <path>" se cierra en minutos, uno que dice "el visor se bloquea al iniciar" no.
Versionen también el binario del motor junto con el ejecutable. PDFium se mueve rápido, y un instalador que actualiza la aplicación mientras deja una biblioteca anterior en disco produce fallas que ninguna computadora de su oficina puede reproducir, porque todas las computadoras de su oficina tienen el par coincidente. Traten la biblioteca como parte del artefacto de compilación: mismo instalador, misma marca de versión, mismo rollback.
Registrar componentes en el IDE Lazarus
La construcción en tiempo de ejecución no necesita registro en diseño, que es la configuración correcta más simple para una aplicación de visor. Si de todos modos quieren los componentes en la paleta de Lazarus, instalen el paquete y dejen que su unidad dedicada de registro (PDFiumLazReg) haga el trabajo: esa unidad está marcada como de diseño en el paquete precisamente porque referencia interfaces de editores de propiedades del IDE que nunca deben vincularse a su ejecutable distribuido.
El síntoma de hacerlo mal es una aplicación que depende misteriosamente de paquetes del IDE, lo que aparece como fallas de despliegue en equipos que nunca han visto Lazarus.
Voz y lectores de pantalla fuera de Windows
Si el original en Delphi ofrecía text-to-speech, el port hereda una decisión de plataforma. SAPI — el backend TTS habitual en Windows — existe solo ahí. Las compilaciones Lazarus dirigidas a Windows conservan comportamiento completo con SAPI y compatible con NVDA, así que un port de Windows a Windows no pierde nada, y los usuarios de NVDA interactúan con el visor de la misma forma bajo cualquiera de los dos IDEs.
Los destinos Linux y macOS necesitan un backend de voz distinto conectado a las mismas APIs de lectura, lo que es un argumento para mantener la voz detrás de una interfaz desde el inicio. La maquinaria de orden de lectura y seguimiento de palabras en sí es neutral de plataforma; solo cambia la capa de salida de audio. El artículo sobre lector accesible cubre esa maquinaria en profundidad.
Qué probar antes de dar por terminado el port
Una pasada de paridad que ha encontrado regresiones reales, en el orden aproximado en que aparecen las fallas: abrir un documento cuya ruta contenga caracteres no ASCII; buscar un término con caracteres no ASCII y confirmar el resaltado de resultados; probar desplazamiento con rueda del mouse, selección por arrastre y navegación de páginas por teclado en cada widget set objetivo, porque foco y rueda son de las áreas más dependientes del widget set en LCL; revisar el renderizado a escalas de pantalla de 100%, 150% y 200%; y ejecutar la compilación instalada — no la compilación del IDE — en una computadora sin el IDE, que es la única prueba que ejercita honestamente la resolución del binario.
Las características de rendimiento de renderizado se transfieren entre ediciones, así que la estrategia de caché del artículo sobre caché de renderizado y rendimiento de zoom aplica sin cambios.
Preguntas frecuentes
¿La edición LCL tiene todas las funciones frente a la edición VCL?
La superficie central — TPdf, TPdfView, renderizado, formularios, extracción de texto y APIs de accesibilidad — es la misma en ambas. Las diferencias reales dependen de plataforma: la salida de voz SAPI es solo Windows, y los diálogos de impresión y archivo siguen las convenciones propias de cada framework.
¿Por qué mi compilación Lazarus se bloquea al iniciar cuando la de Delphi funciona bien?
Revisen primero la resolución del binario: desajuste de arquitectura entre el ejecutable y la biblioteca pdfium, o una ruta de carga que asumió el directorio de trabajo del IDE. Ambas producen fallas inmediatas de inicio que parecen errores del componente y son errores de despliegue.
¿Puedo mantener un formulario compartido entre los IDEs?
Los archivos de descripción de formulario no se transfieren: .dfm y .lfm son formatos distintos con conjuntos de propiedades distintos. Construir la UI del visor en tiempo de ejecución, como se muestra arriba, reemplaza dos archivos de diseñador por una sola ruta de código y elimina el problema de deriva.
Las ediciones VCL y LCL descritas aquí se distribuyen juntas como PDFium Component, con código fuente y APIs públicas idénticas para Delphi, C++Builder y Lazarus/FPC.