pdfviewer — u_pbt_pdfviewer #
← Référence des composants · Sommaire du guide
Visionneuse PDF intégrée : affiche un document local ou publié sur le web, directement dans votre fenêtre, avec pagination, zoom et impression.
▶ Le voir en vrai — Application de démonstration, tuile PDF viewer : l'aperçu, le code qui le produit et cette page, côte à côte.
En bref #
| Userobject | u_pbt_pdfviewer |
| Classe d'items | — (composant sans items) |
| Sert à | Afficher une facture, un bon de commande, un contrat ou une notice sans lancer d'application externe |
| Options opt-in | — |
Le composant remplace le classique « enregistrer le PDF dans un fichier temporaire puis appeler ShellExecute » : le document reste dans votre application, l'utilisateur ne quitte jamais l'écran en cours.
Démarrage rapide #
// event open de la fenetre : afficher un document present sur le disque
uo_pdf.is_source = "C:\factures\FA-2026-0142.pdf"
// event ue_load_completed de uo_pdf : (string as_source)
uo_status.of_panel(/*key*/ "main").is_text = "Document affiche"
C'est tout : poser is_source suffit à charger et afficher le document.
Propriétés #
| Propriété | Type | Défaut | Rôle |
|---|---|---|---|
is_source | string | "" | Document à afficher : un chemin de fichier (absolu, relatif à l'application ou réseau ; un # fait partie du nom), une adresse file:///, une adresse web https://… servie en application/pdf, ou une adresse data:application/pdf. Poser la valeur déclenche le chargement ; poser "" (ou des espaces seuls) vide la visionneuse en gardant ses réglages. Tout le reste est refusé et signalé par ue_load_failed — http:// compris, et une adresse qui porte un nom d'utilisateur et un mot de passe (https://nom:secret@…). Un PDF tenu dans un blob s'affiche par of_load_blob. Se relit tel que vous l'avez écrit |
ii_page | integer | 0 | Page d'ouverture, comptée à partir de 1 (0 = la première page du document). Posée pendant qu'un document est affiché, c'est sa page : il se recharge, ouvert dessus. Posée quand la visionneuse est vide, elle vaut pour le document suivant ; sinon un nouveau document s'ouvre à sa première page. Pour ouvrir un document sur une page, posez is_source puis ii_page : un seul chargement. Chaque changement recharge le document et relève ue_load_completed : le lecteur ne lit sa page qu'au chargement ; la même valeur reposée ne fait rien. En écriture seule : la relire donne la dernière page demandée, pas celle affichée. Le lecteur est celui du moteur web, il ne rend compte de rien. |
ii_zoom | integer | 0 | Zoom en pourcentage, de 25 à 500 — la plage des boutons du lecteur (0 = laissé au lecteur). Une valeur hors de cette plage y est ramenée, et se relit telle qu'appliquée. Poser un zoom annule is_fit, qui le contredit. En écriture seule, comme ii_page : si l'utilisateur zoome avec la barre du lecteur, cette propriété ne suit pas. |
is_fit | string | "" | Ajustement : FIT_PAGE, FIT_WIDTH, FIT_HEIGHT, ou "" pour aucun. Annule ii_zoom |
ib_viewer_toolbar | boolean | true | Affiche la barre du lecteur (numéro de page, zoom, impression, téléchargement). Masquez-la quand votre fenêtre porte elle-même ces commandes |
ib_navigation_pane | boolean | false | Ouvre le panneau latéral du lecteur : les vignettes des pages et les signets du document. Le panneau vient avec la barre du lecteur. Chaque changement recharge le document affiché |
is_destination | string | "" | Ouvre le document sur une destination nommée du PDF — la cible d'un signet, écrite par l'outil qui a produit le fichier. "" pour aucune ; un nom que le document n'a pas l'ouvre à sa première page. Gardée pour les documents suivants tant que vous ne la changez pas. Chaque changement recharge le document affiché |
is_find | string | "" | Un texte du document, surligné là où il apparaît d'abord, le lecteur y défilant : la ligne d'une facture dont parle votre application. "" pour aucun ; un texte absent du document ne change rien. Ce n'est pas une recherche pilotée (ni compte, ni occurrence suivante) : l'utilisateur cherche avec la barre du lecteur. Gardé pour les documents suivants tant que vous ne le changez pas. Chaque changement recharge le document affiché |
ib_allow_save | boolean | true | Offre les commandes Enregistrer et Enregistrer sous de la barre du lecteur et de son menu. À false, le document est montré sans proposer d'en sauver une copie. Chaque visionneuse garde son réglage : celui d'une autre fenêtre n'y change rien. Ce n'est pas une protection : le fichier reste lisible sur le disque. Chaque changement recharge le document affiché ; posé avant is_source, le document ne se charge qu'une fois |
ib_allow_print | boolean | true | Offre la commande Imprimer de la barre du lecteur et de son menu. of_print imprime toujours : c'est votre application qui décide. Chaque visionneuse garde son réglage. Chaque changement recharge le document affiché ; posé avant is_source, le document ne se charge qu'une fois |
ib_allow_search | boolean | true | Offre la commande Rechercher de la barre du lecteur. is_find surligne toujours un texte : c'est votre application qui décide. Chaque visionneuse garde son réglage. Chaque changement recharge le document affiché ; posé avant is_source, le document ne se charge qu'une fois |
ib_allow_rotate | boolean | true | Offre la commande Faire pivoter de la barre du lecteur. Chaque visionneuse garde son réglage. Chaque changement recharge le document affiché ; posé avant is_source, le document ne se charge qu'une fois |
ib_allow_fullscreen | boolean | true | Offre la commande Plein écran de la barre du lecteur. Chaque visionneuse garde son réglage. Chaque changement recharge le document affiché ; posé avant is_source, le document ne se charge qu'une fois |
is_theme_style | string | "" | Style visuel du composant (constantes THEME_STYLE_*) ; vide = celui de l'application, suivi à chaque changement |
is_theme_mode | string | "" | Variante claire ou sombre (constantes THEME_MODE_*) ; vide = celle de l'application, suivie à chaque changement |
il_theme_accent | long | -1 | Couleur d'accent de ce composant (-1 = l'accent de l'application, ou celui du thème) |
Méthodes #
| Méthode | Rôle |
|---|---|
of_refresh ( ) | Relit le document courant (sur le disque ou le réseau) sans toucher aux réglages : le moyen d'afficher un fichier régénéré au même chemin. La position de défilement n'est pas gardée : le lecteur repart à ii_page. Après un échec, retente le même document. Un document refusé est jugé de nouveau : ue_load_failed est relevé. Renvoie 0 une fois demandé, -4 sans document (is_source vide), -2 si le composant n'est pas créé |
of_load_blob (blob ablb_pdf) · of_load_blob (blob ablb_pdf, string as_name) | Affiche un PDF tenu dans un blob — lu en base (SELECTBLOB) ou reçu d'un service — sans écrire de fichier, quelle que soit sa taille : les octets restent en mémoire et ne sont servis qu'à cette visionneuse. Le second argument est le nom que relisent is_source et ue_load_completed / ue_load_failed (document.pdf sans lui). Le blob affiché avant est libéré, celui-ci l'est par le document suivant ou of_reset. Renvoie 0 une fois demandé (ue_load_completed suit), -5 si le blob est vide ou n'est pas un PDF, -4 s'il est trop gros pour ce processus (64 Mo en 32 bits, 512 Mo en 64 bits) — of_get_last_error dit pourquoi —, -2 si le composant n'est pas créé |
of_print ( ) · of_print (boolean) | Ouvre l'aperçu d'impression du document — pas de la page qui l'encadre. Renvoie 0 une fois l'aperçu demandé, -4 quand aucun document n'est affiché, -2 si le composant n'est pas créé. L'argument est sans effet ici : c'est toujours l'aperçu du lecteur PDF |
of_print_to_pdf (string) · of_print_to_pdf (string, boolean) | Rend -4 sur ce composant, sans rien écrire : la page imprimée ne serait que le cadre du lecteur, jamais le document. Le document est déjà un PDF : copiez le fichier de is_source |
of_reset ( ) | Vide la visionneuse et remet toutes les propriétés à leur défaut. Renvoie 0 une fois appliqué, -2 si le composant n'est pas créé |
of_set_redraw (boolean) | Regroupe une rafale de modifications en un seul rendu. Renvoie 0 une fois appliqué, -2 si le composant n'est pas créé |
of_save_as_png (string) · of_save_as_jpg (string) | Exporte le rendu en image. Renvoie 0 une fois l'image écrite, -4 si l'écriture échoue, -2 si le composant n'est pas créé |
Événements #
| Événement | Déclenché quand |
|---|---|
ue_load_completed (string as_source) | Le document est affiché ; as_source est is_source tel que vous l'avez écrit (pour un blob, le nom donné à of_load_blob). Relevé aussi par of_refresh et par chaque changement de page, de zoom, d'ajustement, de barre du lecteur, de panneau, de destination, de texte surligné, de ib_allow_save ou de ib_allow_print — posez-les avant is_source pour un seul chargement ; la même valeur reposée ne le relève pas. Jamais levé sur un échec |
ue_load_failed (string as_source, string as_reason) | Le document n'a pas pu être affiché ; as_reason est l'une des constantes REASON_* ci-dessous. La visionneuse reste vide |
ue_link_clicked (string as_url) | L'utilisateur a suivi un lien du document. La visionneuse reste sur le document : ouvrez as_url où vous voulez (navigateur du poste, webbrowser…). as_url est l'adresse du lien telle quelle (https://…, mailto:…) ; un lien relatif d'un document local arrive en chemin disque. Un lien vers un fichier local du poste (file:///) dans un PDF n'est pas suivi : le lecteur de PDF de WebView2 le bloque ; aucun ue_link_clicked n'est levé |
ue_ready ( ) | Le composant a fini de charger ; tout ce qui a été envoyé avant a été rejoué |
ue_runtime_missing ( ) | Le runtime WebView2 est absent : le composant reste vide |
ue_bg_color (long al_color) | Le composant a calculé sa couleur de fond de thème ; l'userobject l'a déjà adoptée (backcolor) |
Pourquoi un document n'est pas affiché #
as_reason de ue_load_failed est l'une de ces constantes de u_pbt_pdfviewer. Un PDF se reconnaît ainsi : un fichier local porte l'extension .pdf et commence par la signature %PDF- ; un document distant est servi avec le type application/pdf (le seul que le moteur remet à son lecteur) ; une adresse data: annonce application/pdf.
| Constante | Valeur | Cause |
|---|---|---|
REASON_NOT_FOUND | "notfound" | Fichier absent ou illisible, adresse qui répond 404 |
REASON_NOT_PDF | "notpdf" | Ce n'est pas un PDF : fichier local sans la signature %PDF-, réponse distante qui n'est pas application/pdf (un serveur de fichiers qui répond application/octet-stream : téléchargez le document d'abord, voir les exemples), data: ou blob d'un autre type |
REASON_TOO_LARGE | "toolarge" | Fichier trop gros pour ce processus (au-delà de 64 Mo en 32 bits, 512 Mo en 64 bits), ou adresse data: de plus de 2 Mo de caractères (environ 1,5 Mo de PDF) |
REASON_INSECURE | "insecure" | http:// : non pris en charge, servez le document en https:// |
REASON_UNSUPPORTED | "unsupported" | Une autre sorte d'adresse (ftp:, blob:…), ou une adresse qui porte un nom d'utilisateur et un mot de passe (https://nom:secret@…) |
REASON_NETWORK | "network" | Serveur injoignable, nom inconnu, connexion coupée |
REASON_CERTIFICATE | "certificate" | Certificat du site invalide, expiré ou révoqué |
REASON_AUTH | "auth" | Le site ou le proxy demande une authentification : téléchargez le document par le restclient (jeton, identifiants), puis affichez-le (voir les exemples) |
REASON_HTTP | "http" | Une autre erreur du serveur (403, 500…) |
REASON_REFUSED | "refused" | Le site refuse d'être affiché dans un cadre, ou envoie le PDF en téléchargement — le fichier n'est pas téléchargé pour autant |
REASON_FAILED | "failed" | Toute autre cause |
Ce que l'utilisateur peut faire, sans une ligne de code #
La visionneuse affiche sa barre d'outils intégrée au-dessus du document. Vous n'avez rien à programmer : elle est fournie et localisée par le système.
| Action | Comment |
|---|---|
| Pagination | Molette et barre de défilement, ou saisie directe du numéro de page dans le compteur n / total |
| Zoom | Boutons + / −, ajustement à la page ou à la largeur |
| Recherche | Bouton de recherche de la barre d'outils, dans le texte du document |
| Impression | Bouton imprimante de la barre d'outils (masquable par ib_allow_print), ou of_print depuis votre code |
| Enregistrement | Bouton de téléchargement, pour sauver une copie du document (masquable par ib_allow_save) |
| Rotation | Rotation des pages depuis le menu de la barre d'outils |
| Clavier | Dès que le composant a le focus (Tab ou of_focus_webview) : PageDown, flèches, Début / Fin font défiler le document, sans clic préalable |
Les raccourcis du navigateur — Ctrl+F, Ctrl+P, Ctrl + molette — sont coupés dans tous les composants, celui-ci compris : passez par les boutons de la barre du lecteur.
Ce que le lecteur ne dit pas #
Le lecteur est celui qui est intégré au moteur web : aucune dépendance à installer, l'impression, la recherche, les formulaires PDF et le plein écran fournis. En contrepartie, il ne rend compte de rien à votre application : ni la page affichée, ni le nombre de pages, ni le zoom réel, ni le texte sélectionné, et la recherche ne se pilote pas par code (is_find surligne un texte à l'ouverture, rien de plus). ii_page et ii_zoom disent où ouvrir le document, pas où l'utilisateur se trouve. Un PDF protégé par un mot de passe : le lecteur le demande lui-même à l'utilisateur, votre application ne peut pas le lui fournir. Ce choix est délibéré ; un rendu scriptable demanderait une bibliothèque externe.
Exemples #
Ouvrir un document local #
// Chemin absolu, ou relatif au repertoire de l'application
uo_pdf.is_source = "doc\conditions-generales.pdf"
Ouvrir un document publié sur le web #
// Une adresse web se charge exactement comme un fichier local
uo_pdf.is_source = "https://www.monsite.fr/tarifs/catalogue-2026.pdf"
Une connexion internet est évidemment nécessaire ; le chargement est asynchrone, ue_load_completed vous signale la fin.
Afficher le PDF que vient de produire une DataWindow #
// Local variables
string ls_file
// Un fichier par jour, dans le dossier temporaire
ls_file = "C:\temp\report_" + String(Today(), "yyyymmdd") + ".pdf"
// La DataWindow produit le fichier...
dw_report.SaveAs(ls_file, PDF!, false)
// ...et la visionneuse l'affiche immediatement
uo_pdf.is_source = ls_file
Rafraîchir après régénération du fichier #
// Le fichier a ete reecrit au meme emplacement : recharger sans retoucher is_source
uo_pdf.of_refresh()
Enchaîner plusieurs documents dans la même visionneuse #
// event rowfocuschanged de dw_list : (long currentrow)
// Local variables
string ls_pdf
// Le chemin du PDF de la ligne courante
ls_pdf = dw_list.GetItemString(currentrow, "pdf_path")
// Sans piece, la visionneuse se vide (ses reglages gardes) ; sinon elle l'affiche
if ls_pdf = "" then
uo_pdf.is_source = ""
else
uo_pdf.is_source = ls_pdf
end if
Suivre la fin du chargement #
// event ue_load_completed de uo_pdf : (string as_source)
uo_wait.Hide()
// of_print imprime le document affiche
uo_print_button.ib_enabled = true
Dire pourquoi le document n'est pas là #
// event ue_load_failed de uo_pdf : (string as_source, string as_reason)
uo_wait.Hide()
choose case as_reason
case uo_pdf.REASON_NOT_FOUND
uo_status.of_panel(/*key*/ "main").is_text = "Document introuvable : " + as_source
case uo_pdf.REASON_NOT_PDF
uo_status.of_panel(/*key*/ "main").is_text = "Ce fichier n'est pas un PDF"
case else
uo_status.of_panel(/*key*/ "main").is_text = "Document indisponible (" + as_reason + ")"
end choose
Ouvrir ailleurs un lien du document #
// event ue_link_clicked de uo_pdf : (string as_url)
// La visionneuse reste sur le document : le lien s'ouvre dans le navigateur de la fenetre
uo_web.is_address = as_url
Imprimer le document #
// clicked du bouton Imprimer : l'apercu du lecteur PDF, sur le document lui-meme
if uo_pdf.of_print() = -4 then
uo_status.of_panel(/*key*/ "main").is_text = "Aucun document a imprimer"
end if
Montrer sans laisser enregistrer ni imprimer #
// Un document confidentiel : ni Enregistrer ni Imprimer dans la barre du lecteur
uo_pdf.ib_allow_save = false
uo_pdf.ib_allow_print = false
uo_pdf.is_source = is_current_document
Posez-les avant is_source : le document ne se charge qu'une fois. Chaque visionneuse garde son réglage. Ce n'est pas une protection : le fichier reste lisible sur le disque, et of_print imprime toujours.
Aperçu dans un onglet, à côté de la saisie #
// event open : la visionneuse occupe une page d'onglet, la saisie l'autre
uo_tab.of_add_page(/*key*/ "entry", /*title*/ "Saisie", /*page*/ uo_page_entry)
uo_tab.of_add_page(/*key*/ "preview", /*title*/ "Apercu", /*page*/ uo_page_preview)
// La visionneuse est posee dans uo_page_preview comme n'importe quel controle
uo_pdf.is_source = is_current_document
Le composant s'héberge sans précaution particulière dans un tab ou un panneau dockcontainer.
Vérifier le fichier avant de l'afficher #
// Local variables
string ls_path
// Le fichier de la facture affichee
ls_path = "C:\factures\" + is_number + ".pdf"
// Pas de fichier, rien a montrer : on vide la visionneuse plutot que de laisser l'ancien document
if not FileExists(ls_path) then
uo_pdf.is_source = ""
uo_status.of_panel(/*key*/ "main").is_text = "Facture introuvable"
return
end if
// Sinon, la facture s'affiche
uo_pdf.is_source = ls_path
Afficher un PDF lu en base (blob) #
// Local variables
blob lblb_pdf
// La facture rangee en base, lue dans un blob
SELECTBLOB pdf_file INTO :lblb_pdf FROM invoice WHERE invoice_no = :is_number USING SQLCA;
// Affichee directement depuis la memoire : aucun fichier temporaire, quelle que soit sa taille
if SQLCA.SQLCode = 0 then
uo_pdf.of_load_blob(/*pdf*/ lblb_pdf, /*name*/ "invoice-" + is_number + ".pdf")
end if
Le nom donné revient dans is_source et dans ue_load_completed ; le blob est libéré par le document suivant ou par of_reset.
Montrer une ligne précise du document #
// Le lecteur s'ouvre sur le premier passage de ce texte, surligne
uo_pdf.is_find = "Delivery and assembly"
uo_pdf.is_source = "C:\factures\FA-2026-0142.pdf"
Ouvrir sur un chapitre, signets visibles #
// Le panneau lateral et ses signets, ouvert sur le chapitre "prices" du catalogue
uo_pdf.ib_navigation_pane = true
uo_pdf.is_destination = "prices"
uo_pdf.is_source = "doc\catalogue-2026.pdf"
Le nom d'une destination est celui que l'outil qui a produit le PDF a écrit : votre générateur de documents le connaît.
Un document derrière un jeton, ou servi en octet-stream #
// Local variables
long ll_status
string ls_file
// Le restclient (inv_rest, prepare a l'ouverture de la fenetre) envoie le jeton et telecharge le fichier
ls_file = "C:\temp\invoice-4152.pdf"
ll_status = inv_rest.of_download(/*url*/ "/orders/4152/invoice.pdf", /*path*/ ls_file)
// Puis la visionneuse affiche le fichier, quel que soit le type annonce par le serveur
if ll_status = 200 then
uo_pdf.is_source = ls_file
end if
Le lecteur du moteur ne sait ni envoyer un jeton ni afficher une réponse application/octet-stream (REASON_AUTH, REASON_NOT_PDF) : le restclient le fait pour lui.
Formats et chemins acceptés #
Forme de is_source | Exemple | Remarque |
|---|---|---|
| Chemin absolu | "C:\docs\contrat.pdf" | Le plus fiable |
| Chemin relatif | "doc\notice.pdf" | Relatif au répertoire de l'application |
| Chemin réseau | "\\serveur\partage\bon.pdf" | L'utilisateur doit avoir les droits de lecture |
Nom avec # | "C:\devis\Devis #12.pdf" | Le # fait partie du nom du fichier |
Adresse file: | "file:///C:/docs/contrat.pdf" | Convertie en chemin, comme un chemin absolu ; file://localhost/C:/… et la forme UNC à quatre barres file:////serveur/partage/… aussi |
| Adresse web | "https://…/catalogue.pdf" | Servie en application/pdf, chargement asynchrone. Un #page=… écrit dans l'adresse est ignoré : utilisez ii_page. Une adresse qui porte des identifiants (https://nom:secret@…) est refusée (REASON_UNSUPPORTED) : passez par le restclient |
| Document en mémoire | "data:application/pdf;base64,…" | Rien n'est écrit sur le disque. Au-delà de 2 Mo de caractères (environ 1,5 Mo de PDF), ue_load_failed avec REASON_TOO_LARGE : pour un document en mémoire plus gros, of_load_blob |
Adresse http:// | "http://intranet/bon.pdf" | Non prise en charge : ue_load_failed avec REASON_INSECURE. Servez le document en https:// |
| Vide | "" | Vide la visionneuse et garde ses réglages ; une valeur faite d'espaces aussi |
Seul le PDF est pris en charge par ce composant : ce qui n'en est pas un est refusé et signalé par ue_load_failed (REASON_NOT_PDF). Pour une image, utilisez picture ; pour une page HTML, webbrowser.
Bonnes pratiques #
- Surveillez
ue_load_failed: un chemin invalide, un fichier qui n'est pas un PDF ou un site injoignable y est signalé avec sa raison, et la visionneuse reste vide. - Pour vider la visionneuse (changement de ligne sans pièce jointe), posez
is_source = "": sinon le document précédent reste visible.of_reset()la vide aussi, mais remet en plus tous les réglages à leur défaut — Enregistrer et Imprimer compris. of_refresh()est le moyen d'afficher un fichier régénéré au même chemin : il relit le fichier sans toucher aux réglages. Il ne garde pas la position de défilement — le lecteur repart àii_page.- Chaque changement de
ii_zoom,is_fit,ib_viewer_toolbar,ib_navigation_pane,is_destination,is_find,ib_allow_saveouib_allow_printrecharge le document (le lecteur ne lit ses réglages qu'au chargement) et relèveue_load_completed: posez-les avantis_sourcepour un seul chargement. La même valeur reposée ne recharge rien.ii_pageposé pendant qu'un document est affiché vaut pour ce document : pour en ouvrir un autre sur une page, posezis_sourcepuisii_page. - Prévoyez un indicateur d'attente pour les documents distants ou volumineux, et masquez-le sur
ue_load_completedet surue_load_failed. - Donnez au composant une surface confortable (au moins la moitié de la fenêtre) : la barre d'outils intégrée et le document ont besoin de place pour rester lisibles.
- Pour afficher une page web plutôt qu'un PDF, utilisez webbrowser ; pour une image, picture.
Hérité du socle commun #
Ces membres existent sur tous les composants visuels — ils ne sont pas propres à celui-ci. Ils sont détaillés une seule fois, dans les chapitres transverses ; cette table dit seulement où les lire.
| Membres | Rôle | Détaillé dans |
|---|---|---|
of_reset | Remettre le composant à zéro | 3.6 Remettre un composant à zéro : of_reset() |
of_register_shortcut · of_clear_shortcuts | Raccourcis clavier du composant | 3.5 Les raccourcis clavier |
of_is_created · of_is_ready · of_get_last_error | S'il est né, s'il est prêt, ce qui a échoué | 3.7 Diagnostic |
of_save_as_png · of_save_as_jpg | Exporter le rendu en image | 3.8 Exporter le rendu en image |
of_set_redraw | Grouper les modifications en un seul repaint | 3.10 Bonnes pratiques |
of_preload_icons | Icônes affichées sans délai | Affichage instantané : of_icon |
of_set_translation | Traduire un libellé du composant | 5.2 Adapter un libellé : of_set_translation |
of_focus_webview | Donner le focus au composant | 6.4 Clavier et focus |
of_print · of_print_to_pdf | Imprimer, ou écrire un PDF | 6.9 Imprimer |
of_set_property · of_get_property · of_component_name | Piloter une propriété par son nom | 3.1 Le moteur de propriétés |
is_accessible_name · is_accessible_description | Le nom et la description que dit un lecteur d'écran | 3.4 Propriétés et events optionnels |
Deux aides ne sont pas héritées : of_icon et of_escape_markup vivent sur n_pbt_utils. Déclarez-en une — n_pbt_utils lnv_utils, rien à créer — et appelez-les dessus.