diff --git a/src/application/planche_seance.test.js b/src/application/planche_seance.test.js index 00cf022..c4bda18 100644 --- a/src/application/planche_seance.test.js +++ b/src/application/planche_seance.test.js @@ -13,12 +13,15 @@ // productions de PDF au plus dans ce fichier, sur une soirée de quatre // personnes — trois pour les trois modes, trois pour la stabilité des // octets, une réédition, deux refus d'écriture et l'enregistrement sous, -// une sous web, une proposition. Celle sous web sort en Lettre, en pile, -// par table : sur deux badges par planche, l'imposition et l'ordre demandés -// se lisent dans la place de chaque nom. La réédition porte un caractère -// sans glyphe, que sa feuille de contrôle nomme. Le témoin de 257 ne -// s'analyse que pour son annonce. Les noms sont inventés ; la version vient -// du module engendré. +// une sous web, une proposition. L'imposition et l'ordre se lisent dans la +// place de chaque nom, et le papier, l'imposition et le nombre de planches +// dans la feuille de contrôle : la première production, sous la demande +// par défaut, en A4 — l'ordre alphabétique ; la proposition, sous la +// demande par défaut, en Lettre — feuille par feuille sur deux planches ; +// celle sous web en Lettre, en pile, par table. La réédition porte un +// caractère sans glyphe, que sa feuille de contrôle nomme. Le témoin de 257 +// ne s'analyse que pour son annonce. Les noms sont inventés ; la version +// vient du module engendré. import assert from 'node:assert/strict'; import { creerFichiersSimules } from '../../test/fichiers_simules.js'; import { H_PROPOSITION, H_RETENU, personne, soiree, table } from '../../test/fixtures/badges/cas.js'; @@ -26,6 +29,8 @@ import { FORME_TEMOIN, chargeTemoin } from '../../test/fixtures/badges/temoin.js import { describe, test } from '../../test/lanceur.js'; import { lirePdf } from '../../test/pdf.js'; import { lirePolices } from '../../test/polices.js'; +import { PAPIERS } from '../geometrie/papiers.js'; +import { cellules } from '../pdf/imposition.js'; import { serialiser } from '../stockage/canonique.js'; import { creer as creerDansLeDepot } from '../stockage/depot.js'; import { FORMAT } from '../stockage/document.js'; @@ -161,17 +166,33 @@ async function refusee(promesse, code, details) { // Les textes d'une page du PDF relu, et si l'un d'eux s'écrit au trait. const textesDe = (page) => page.textes.map(({ texte }) => texte); - -// Les noms d'une page de badges, parmi ceux donnés, du haut vers le bas puis -// de gauche à droite : l'ordre des cellules sur la feuille. Dans les boîtes -// que rend lirePdf, y croît vers le pied de la page. -const nomsDe = (page, noms) => - page.textes - .filter(({ texte }) => noms.includes(texte)) - .sort((a, b) => a.y - b.y || a.x - b.x) - .map(({ texte }) => texte); const auTrait = (pdf) => pdf.pages.some((page) => page.modesTexte.includes(1)); +// Les noms d'une page de badges, parmi ceux donnés, cellule par cellule +// dans l'ordre de cellules(papier) ; null pour une cellule où aucun ne se +// lit. Un nom appartient à la cellule qui contient le centre de sa boîte. +const NOMS = ['Iris Ombrelle', 'Théo Pervenche', 'Ondine Lacasse', 'Grisaille']; +function nomsParCellule(page, papier, noms = NOMS) { + return cellules(papier).map(({ x, y, largeur, hauteur }) => { + const lus = page.textes + .filter((t) => noms.includes(t.texte)) + .filter((t) => t.x + t.l / 2 > x && t.x + t.l / 2 < x + largeur && t.y + t.h / 2 > y && t.y + t.h / 2 < y + hauteur) + .map(({ texte }) => texte); + assert.ok(lus.length <= 1, `plusieurs noms dans une cellule : ${lus.join(', ')}`); + return lus[0] ?? null; + }); +} + +// Les lignes de la feuille de contrôle qui disent le papier, l'imposition et +// le nombre de planches d'une production. +const lignesDeSortie = ({ papier, imposition, planches }) => [ + libelle('planche.controle.papier', { papier, ...PAPIERS[papier] }), + libelle('planche.controle.imposition', { imposition }), + libelle('planche.controle.planches', { planches }), +]; +// Celles que porte la page de contrôle relue, parmi les trois. +const sortieLue = (pdf, sortie) => lignesDeSortie(sortie).filter((ligne) => textesDe(pdf.pages[0]).includes(ligne)); + describe('planche dans la séance : sortir n’est pas modifier (contrainte 24, § 8.4, § 9)', () => { test('produire en lecture, sur un plan bloqué, sous un format plus récent : la trace ne porte que le .pdf, déposé à côté de l’événement', async () => { const cas = [ @@ -213,6 +234,12 @@ describe('planche dans la séance : sortir n’est pas modifier (contrainte 24, const pdf = await lirePdf(retenu); assert.ok(!pdf.pages.some((page) => textesDe(page).includes(FILIGRANE))); assert.equal(auTrait(pdf), false); + // La demande par défaut : l'ordre alphabétique, par nom puis prénom, sur + // l'unique planche A4, feuille par feuille. + const sortie = { papier: 'A4', imposition: 'feuille', planches: 1 }; + assert.deepEqual(sortieLue(pdf, sortie), lignesDeSortie(sortie), textesDe(pdf.pages[0]).join(' | ')); + assert.equal(pdf.pages.length, 2); + assert.deepEqual(nomsParCellule(pdf.pages[1], 'A4'), ['Grisaille', 'Ondine Lacasse', 'Iris Ombrelle', 'Théo Pervenche']); }); test('sans papier choisi : PAPIER_REQUIS avant toute écriture ; choisi, le nom et l’annonce le portent, et une autre séance de la racine ne le redemande pas', async () => { @@ -376,13 +403,25 @@ describe('planche dans la séance : les octets et le nom (§ 11.6, § 11.8)', () assert.equal(pdf.pages.length, 2); }); - test('une proposition produite porte le mot du filigrane au trait ; sa feuille de contrôle aussi', async () => { - const { fs, seance } = await monter(); + test('une proposition produite porte le mot du filigrane au trait, sa feuille de contrôle aussi ; en Lettre sous la demande par défaut, feuille par feuille, dans l’ordre alphabétique', async () => { + const { fs, seance } = await monter(chargeLucioles(), { papier: 'Lettre' }); const rendu = await seance.produirePlanche({ source: 'proposition', proposition: 2 }); - assert.equal(rendu.nom, nomDe({ horodatage: H_PROPOSITION })); + assert.equal(rendu.nom, nomDe({ papier: 'Lettre', horodatage: H_PROPOSITION })); const pdf = await lirePdf(fs.octets('documents', rendu.nom)); assert.ok(pdf.pages.every((page) => textesDe(page).includes(FILIGRANE))); assert.equal(auTrait(pdf), true); + // Deux badges par planche : feuille par feuille, la planche p porte les + // badges 2p − 1 et 2p de l'ordre alphabétique — en pile, ce seraient p + // et p + 2 ; par table, la proposition 2 assied Ombrelle avec Grisaille. + const sortie = { papier: 'Lettre', imposition: 'feuille', planches: 2 }; + assert.deepEqual(sortieLue(pdf, sortie), lignesDeSortie(sortie), textesDe(pdf.pages[0]).join(' | ')); + assert.deepEqual( + pdf.pages.slice(1).map((page) => nomsParCellule(page, 'Lettre')), + [ + ['Grisaille', 'Ondine Lacasse'], + ['Iris Ombrelle', 'Théo Pervenche'], + ], + ); }); }); @@ -448,10 +487,11 @@ describe('planche dans la séance : l’écriture refusée, enregistrer sous, le // Grisaille, Lacasse (table 2) ; en pile sur deux planches, la planche p // porte les badges p et p + 2 de cet ordre. const pdf = await lirePdf(recus[0][1]); - const noms = ['Iris Ombrelle', 'Théo Pervenche', 'Ondine Lacasse', 'Grisaille']; assert.equal(pdf.pages.length, 3); + const sortie = { papier: 'Lettre', imposition: 'pile', planches: 2 }; + assert.deepEqual(sortieLue(pdf, sortie), lignesDeSortie(sortie), textesDe(pdf.pages[0]).join(' | ')); assert.deepEqual( - pdf.pages.slice(1).map((page) => nomsDe(page, noms)), + pdf.pages.slice(1).map((page) => nomsParCellule(page, 'Lettre')), [ ['Iris Ombrelle', 'Grisaille'], ['Théo Pervenche', 'Ondine Lacasse'], diff --git a/src/pdf/planche.long.test.js b/src/pdf/planche.long.test.js index 82ba7d4..96cf1e7 100644 --- a/src/pdf/planche.long.test.js +++ b/src/pdf/planche.long.test.js @@ -22,6 +22,7 @@ import { cadreUtile, cellules, filets, imposer, nombrePlanches } from './imposit import { produirePlanche } from './planche.js'; const ZONE = constante('ZONE_SILENCE_MM'); +const MARGE = constante('MARGE_PAGE_MM'); const TOLERANCE = 1e-6; const FILIGRANE = libelle('planche.brouillon'); const polices = lirePolices(); @@ -156,7 +157,7 @@ describe('produirePlanche : la géométrie du Lettre et le témoin (§ 11.2, § }); describe('produirePlanche : une feuille de contrôle plus longue qu\u{2019}une page (§ 11.3)', () => { - test('les lignes qui débordent continuent sur une page de contrôle sans règle ni filet, avant les planches', async () => { + test('les lignes qui débordent continuent sur une page de contrôle sans règle ni filet, avant les planches, chaque page remplie dans ses marges', async () => { const m = modele(150, { brouillon: true, reedition: true }); const controle = textesControle(m, { papier: 'A4', imposition: 'feuille', planches: nombrePlanches(150, 'A4') }); const lu = await produireEtLire(m, 'A4', 'feuille'); @@ -173,6 +174,16 @@ describe('produirePlanche : une feuille de contrôle plus longue qu\u{2019}une p assert.deepEqual(controleLu, ['0', '50', '100', ...controle]); for (const page of lu.pages.slice(0, pagesControle)) assert.equal(page.textes.filter(estFiligrane).length, 1); for (const page of lu.pages.slice(1, pagesControle)) assert.deepEqual(page.segments, []); + // Chaque texte des pages de contrôle dans les quatre marges : la page + // de suite commence sous la marge haute, et une page ne se remplit que + // jusqu'à la marge basse — la ligne qui la passerait part à la suivante. + const { largeur: W, hauteur: H } = PAPIERS.A4; + lu.pages.slice(0, pagesControle).forEach((page, rang) => { + for (const { texte, x, y, l, h } of page.textes) { + const dedans = x >= MARGE - TOLERANCE && y >= MARGE - TOLERANCE && x + l <= W - MARGE + TOLERANCE && y + h <= H - MARGE + TOLERANCE; + assert.ok(dedans, `page ${rang + 1} : « ${texte} » en ${JSON.stringify({ x, y, l, h })}`); + } + }); const attendu = imposer(150, 'A4', 'feuille').map((planche) => planche.map((i) => (i === null ? null : nomTemoin(i)))); assert.deepEqual(nomsLus(lu.pages.slice(pagesControle), 'A4'), attendu); }); diff --git a/src/pdf/planche.test.js b/src/pdf/planche.test.js index b48743a..7960825 100644 --- a/src/pdf/planche.test.js +++ b/src/pdf/planche.test.js @@ -12,9 +12,13 @@ // petits modèles écrits à la main, que toutes les épreuves du fichier // lisent — A4, cinq badges, feuille par feuille, brouillon, horodatage // connu, produite deux fois ; Lettre, trois badges, en pile, retenu, -// horodatage inconnu. Les lignes de contrôle viennent de textesControle, le -// mot du filigrane de libelle, comme la séance les passera. +// horodatage inconnu, sous un nom d'événement plus large que la page, dont +// la ligne de contrôle se replie aux espaces, reporte entier un mot qui +// tient seul, puis coupe un mot qui ne tient pas seul. Les lignes de +// contrôle viennent de textesControle, le mot du filigrane de libelle, +// comme la séance les passera. import assert from 'node:assert/strict'; +import fontkit from '@pdf-lib/fontkit'; import { PDFDocument, PDFName } from 'pdf-lib'; import { lirePdf } from '../../test/pdf.js'; import { lirePolices } from '../../test/polices.js'; @@ -25,6 +29,7 @@ import { constante } from '../geometrie/constantes.js'; import { PAPIERS } from '../geometrie/papiers.js'; import { VERSION } from '../version.genere.js'; import { cellules, filets, imposer, nombrePlanches } from './imposition.js'; +import { metriquesDe } from './mise_en_page.js'; import { analyserPlanche, produirePlanche } from './planche.js'; const TOLERANCE = 1e-6; @@ -35,10 +40,18 @@ const TIRET = '\u{2014}'; const polices = lirePolices(); const EVENEMENT = { nom: 'Souper du Club des Merles', date: '2026-10-09' }; +// Un nom d'événement plus large que la largeur utile d'une page : des mots, +// dont un mot long qui tient seul sur une ligne mais déborde en fin de +// ligne là où quelques-unes de ses grappes tiendraient encore, puis un mot +// composé sans espace, plus large à lui seul qu'une ligne. +const EVENEMENT_LONG = { + nom: `Grand souper annuel des amis du Club des Merles et de la Pervenche, au profit des Ombrellebleuetières, Merles${'-Lucioles'.repeat(14)}`, + date: '2026-10-09', +}; const HORODATAGE = '2026-10-09T19:00:00-04:00'; /** Un badge écrit à la main : ses textes tels que BA1 les compose. */ -function badge(participant, nom, { appartenance = null, tours = 2, horodatage = HORODATAGE } = {}) { +function badge(participant, nom, { appartenance = null, tours = 2, horodatage = HORODATAGE, evenement = EVENEMENT } = {}) { const lignes = Array.from({ length: tours }, (_, i) => ({ tour: i + 1, table: participant + i, siege: null, titre: null })); const ligneHorodatage = horodatage === null ? libelle('planche.pied.horodatageInconnu') : libelle('planche.pied.horodatage', { horodatage }); return { @@ -51,17 +64,17 @@ function badge(participant, nom, { appartenance = null, tours = 2, horodatage = nom, lignes: lignes.map((ligne) => libelle('planche.tour', ligne)), appartenance, - pied: [libelle('planche.pied.evenement', EVENEMENT), ligneHorodatage], - piedReduit: [libelle('planche.pied.date', EVENEMENT), ligneHorodatage], + pied: [libelle('planche.pied.evenement', evenement), ligneHorodatage], + piedReduit: [libelle('planche.pied.date', evenement), ligneHorodatage], }, }; } /** Un modèle de planche écrit à la main. */ -function modele(badges, { horodatage = HORODATAGE, brouillon = false, exclus = 0 } = {}) { +function modele(badges, { horodatage = HORODATAGE, brouillon = false, exclus = 0, evenement = EVENEMENT } = {}) { return { badges, - evenement: EVENEMENT, + evenement, horodatage, brouillon, comptes: { participants: badges.length + exclus, exclus, produits: badges.length }, @@ -78,8 +91,8 @@ const MODELE_A4 = modele( ); const NOMS_LETTRE = ['Aube Sarcelle', 'Côme Pervenche', 'Ysée Lacasse']; const MODELE_LETTRE = modele( - NOMS_LETTRE.map((nom, i) => badge(i + 10, nom, { horodatage: null, tours: 3 })), - { horodatage: null }, + NOMS_LETTRE.map((nom, i) => badge(i + 10, nom, { horodatage: null, tours: 3, evenement: EVENEMENT_LONG })), + { horodatage: null, evenement: EVENEMENT_LONG }, ); const entrees = (m, papier, imposition) => ({ @@ -329,6 +342,17 @@ describe('produirePlanche : les badges (§ 11.3, § 11.4, § 11.6)', () => { }); }); +// La mesure de la police embarquée, celle que la production emploie : +// les deux graisses chargées par pdf-lib et fontkit dans un document +// neuf, qui ne s'enregistre pas. +const documentDeMesure = await PDFDocument.create({ updateMetadata: false }); +documentDeMesure.registerFontkit(fontkit); +const metriques = metriquesDe({ + regulier: await documentDeMesure.embedFont(polices.regulier, { subset: true }), + gras: await documentDeMesure.embedFont(polices.gras, { subset: true }), +}); +const MM_PAR_PT = 25.4 / 72; + describe('produirePlanche : la feuille de contrôle, page 1 (§ 11.3)', () => { // La graduation : le segment horizontal de 100 mm, et les repères qui le // touchent, verticaux. @@ -343,6 +367,48 @@ describe('produirePlanche : la feuille de contrôle, page 1 (§ 11.3)', () => { return { regle, reperes }; } + // Les lignes de contrôle d'une page : ses fragments dans l'ordre du flux, + // sans les chiffres de la règle, posés d'abord, ni le filigrane, que sa + // rotation rend plus haut qu'aucune ligne. + const CHIFFRES_REGLE = ['0', '50', '100']; + function fragmentsDeControle(page) { + const sansFiligrane = page.textes.filter(({ texte, h }) => !(texte === FILIGRANE && h > 20)); + assert.deepEqual(sansFiligrane.slice(0, CHIFFRES_REGLE.length).map(({ texte }) => texte), CHIFFRES_REGLE); + return sansFiligrane.slice(CHIFFRES_REGLE.length); + } + + // Les fragments rapportés aux lignes attendues, dans l'ordre : chaque ligne + // se lit d'un ou de plusieurs fragments consécutifs, accolés quand la coupe + // tombe dans un mot, séparés par une espace quand elle tombe sur une + // espace — aucun caractère perdu, ajouté ni déplacé, aucun fragment de + // reste. Rend, par ligne, ses morceaux, l'indice dans la ligne où chacun + // commence et la nature de chaque coupe. + function recomposer(fragments, lignes) { + let k = 0; + const recomposees = lignes.map((ligne) => { + const morceaux = []; + const debuts = []; + const coupes = []; + let position = 0; + while (position < ligne.length) { + const fragment = fragments[k]; + assert.ok(fragment !== undefined && ligne.startsWith(fragment.texte, position), `« ${ligne} » à ${position} : « ${fragment?.texte} »`); + morceaux.push(fragment); + debuts.push(position); + k += 1; + position += fragment.texte.length; + if (position < ligne.length) { + const surEspace = ligne[position] === ' '; + coupes.push(surEspace ? 'espace' : 'mot'); + if (surEspace) position += 1; + } + } + return { ligne, morceaux, debuts, coupes }; + }); + assert.deepEqual(fragments.slice(k).map(({ texte }) => texte), [], 'fragments de reste'); + return recomposees; + } + test('graduation de 0 à 100 mm, repérée tous les 10 mm, mesurée sur les segments', () => { for (const { papier, lu } of PRODUCTIONS) { const { regle, reperes } = graduation(lu.pages[0]); @@ -356,11 +422,10 @@ describe('produirePlanche : la feuille de contrôle, page 1 (§ 11.3)', () => { } }); - test("les lignes de contrôle se lisent dans la couche de texte : comptes, horodatage, version, papier", () => { + test("les lignes de contrôle se lisent dans la couche de texte, dans l'ordre et entières : comptes, horodatage, version, papier", () => { for (const { papier, imposition, lu, modele: m } of PRODUCTIONS) { - const lus = new Set(lu.pages[0].textes.map(({ texte }) => texte)); const lignes = textesControle(m, { papier, imposition, planches: nombrePlanches(m.badges.length, papier) }); - for (const ligne of lignes) assert.ok(lus.has(ligne), `${papier} : « ${ligne} » absente de ${[...lus].join(' | ')}`); + recomposer(fragmentsDeControle(lu.pages[0]), lignes); } const lusA4 = A4.pages[0].textes.map(({ texte }) => texte); assert.ok(lusA4.includes(libelle('planche.controle.comptes', MODELE_A4.comptes))); @@ -368,6 +433,64 @@ describe('produirePlanche : la feuille de contrôle, page 1 (§ 11.3)', () => { assert.ok(LETTRE.pages[0].textes.some(({ texte }) => texte === libelle('planche.pied.horodatageInconnu'))); }); + test("une ligne plus large que la page se replie dans la largeur utile : aux espaces, puis dans le mot qui ne tient pas seul ; chaque morceau aussi plein qu'il se peut", () => { + const largeurUtile = PAPIERS.Lettre.largeur - 2 * MARGE; + const lignes = textesControle(MODELE_LETTRE, { papier: 'Lettre', imposition: 'pile', planches: nombrePlanches(3, 'Lettre') }); + const recomposees = recomposer(fragmentsDeControle(LETTRE.pages[0]), lignes); + const evenement = recomposees.find(({ ligne }) => ligne === libelle('planche.controle.evenement', EVENEMENT_LONG)); + assert.ok(evenement.coupes.includes('espace') && evenement.coupes.includes('mot'), JSON.stringify(evenement.coupes)); + for (const { ligne, morceaux, debuts, coupes } of recomposees) { + if (ligne !== evenement.ligne) assert.equal(morceaux.length, 1, ligne); + const corps = morceaux[0].h / MM_PAR_PT; + const largeur = (texte) => metriques.largeur(texte, corps, false); + for (const morceau of morceaux) { + // La mesure de l'épreuve est celle du fichier : l'avance relue. + assert.ok(Math.abs(largeur(morceau.texte) - morceau.l) < 0.01, `« ${morceau.texte} » : ${largeur(morceau.texte)} contre ${morceau.l}`); + assert.ok(morceau.l <= largeurUtile + TOLERANCE, `« ${morceau.texte} » large de ${morceau.l} mm`); + } + // Une coupe ne tombe que là où la suite déborde : le premier mot (ou + // la première grappe) du morceau suivant, ajouté, passe la largeur. + coupes.forEach((coupe, i) => { + const suivant = morceaux[i + 1].texte; + const ajout = coupe === 'espace' ? ` ${suivant.split(' ')[0]}` : suivant.match(/\P{M}\p{M}*/u)[0]; + assert.ok(largeur(morceaux[i].texte + ajout) > largeurUtile, `coupe ${coupe} trop tôt après « ${morceaux[i].texte} »`); + }); + // Une coupe dans un mot ne tombe que dans un mot plus large à lui seul + // que la ligne, et ce mot ouvre le morceau qui porte son début : un mot + // qui tient seul passe entier à la ligne suivante. + coupes.forEach((coupe, i) => { + if (coupe !== 'mot') return; + const position = debuts[i] + morceaux[i].texte.length; + const debutMot = ligne.lastIndexOf(' ', position - 1) + 1; + const finMot = ligne.indexOf(' ', position); + const mot = ligne.slice(debutMot, finMot === -1 ? ligne.length : finMot); + assert.ok(largeur(mot) > largeurUtile, `« ${mot} », large de ${largeur(mot)} mm, coupé après « ${morceaux[i].texte} »`); + const porteur = debuts.findLastIndex((debut) => debut <= debutMot); + assert.equal(debuts[porteur], debutMot, `« ${mot} » commence dans « ${morceaux[porteur].texte} »`); + }); + } + // Les données éprouvent le report d'un mot entier : le mot long, qui + // tient seul sur une ligne, ouvre un morceau alors que sa première + // grappe tenait encore au bout du morceau précédent. + const long = 'Ombrellebleuetières,'; + const largeurLue = (texte) => metriques.largeur(texte, evenement.morceaux[0].h / MM_PAR_PT, false); + const avant = evenement.morceaux.findIndex(({ texte }) => texte.startsWith(long)) - 1; + assert.ok(avant >= 0, JSON.stringify(evenement.morceaux.map(({ texte }) => texte))); + assert.ok(largeurLue(long) <= largeurUtile && largeurLue(`${evenement.morceaux[avant].texte} ${long[0]}`) <= largeurUtile); + }); + + test('chaque texte de la feuille de contrôle, chiffres et filigrane compris, tient dans les marges ; chaque ligne part de la marge gauche', () => { + for (const { papier, lu } of PRODUCTIONS) { + const { largeur: W, hauteur: H } = PAPIERS[papier]; + const [page] = lu.pages; + for (const { texte, x, y, l, h } of page.textes) { + const dedans = x >= MARGE - TOLERANCE && y >= MARGE - TOLERANCE && x + l <= W - MARGE + TOLERANCE && y + h <= H - MARGE + TOLERANCE; + assert.ok(dedans, `${papier} : « ${texte} » en ${JSON.stringify({ x, y, l, h })}`); + } + for (const { texte, x } of fragmentsDeControle(page)) assert.ok(Math.abs(x - MARGE) < TOLERANCE, `${papier} : « ${texte} » part de ${x} mm`); + } + }); + test('BROUILLON sur la feuille de contrôle du brouillon : sa ligne et le filigrane, une fois, hors de la graduation', () => { const page = A4.pages[0]; const marques = page.textes.filter(({ texte }) => texte === FILIGRANE);