using System.IO.Packaging; using System.Text.Json; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using Omika.Core.Contracts.Documents; using Omika.Core.Services; using WP = DocumentFormat.OpenXml.Drawing.Wordprocessing; namespace Omika.Tests.Services; /// /// Tests de l'insertion d'images ChpImage (Base64 / fichier) dans le DOCX par /// : Drawing OpenXML + ImagePart, suppression du /// placeholder, non-régression des champs texte, conservation du ratio. /// public class RapportPdfServiceImageTests { // ------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------ /// Construit des octets « PNG-like » (magic bytes + dimensions) pour les tests DOCX. private static byte[] FakePng(int largeur, int hauteur) { byte[] png = new byte[24]; png[0] = 0x89; png[1] = 0x50; png[2] = 0x4E; png[3] = 0x47; png[4] = 0x0D; png[5] = 0x0A; png[6] = 0x1A; png[7] = 0x0A; png[12] = 0x49; png[13] = 0x48; png[14] = 0x44; png[15] = 0x52; // "IHDR" png[16] = (byte)(largeur >> 24); png[17] = (byte)(largeur >> 16); png[18] = (byte)(largeur >> 8); png[19] = (byte)largeur; png[20] = (byte)(hauteur >> 24); png[21] = (byte)(hauteur >> 16); png[22] = (byte)(hauteur >> 8); png[23] = (byte)hauteur; return png; } private static byte[] CreerDocx(params string[] paragraphes) { using var stream = new MemoryStream(); using (WordprocessingDocument doc = WordprocessingDocument.Create(stream, WordprocessingDocumentType.Document)) { MainDocumentPart main = doc.AddMainDocumentPart(); main.Document = new Document(); Body body = new Body(); foreach (string texte in paragraphes) { body.Append(new Paragraph(new Run(new Text(texte)))); } main.Document.Append(body); main.Document.Save(); } return stream.ToArray(); } private static byte[] CreerDocxAvecTableau(params string[] cellules) { using var stream = new MemoryStream(); using (WordprocessingDocument doc = WordprocessingDocument.Create(stream, WordprocessingDocumentType.Document)) { MainDocumentPart main = doc.AddMainDocumentPart(); main.Document = new Document(); Body body = new Body(); var table = new Table(); var row = new TableRow(); foreach (string cellule in cellules) { row.Append(new TableCell(new Paragraph(new Run(new Text(cellule))))); } table.Append(row); body.Append(table); main.Document.Append(body); main.Document.Save(); } return stream.ToArray(); } private static RapportChampDto ChampImage(string rubrique, string? valeur, string? membreMob = null) => new() { Id = "1", Rubrique = rubrique, Type = "ChpImage", TypeMembre = "ChpImage", MembreMob = membreMob, ListeDonnesBD = valeur }; private static (int ImageParts, int Drawings, string Texte, IReadOnlyList Extents) Inspecter(byte[] docx) { int imageParts; using (var pkgStream = new MemoryStream(docx)) using (Package package = Package.Open(pkgStream, FileMode.Open, FileAccess.Read)) { imageParts = package.GetParts() .Count(p => p.Uri.OriginalString.StartsWith("/word/media/", StringComparison.Ordinal)); } using var stream = new MemoryStream(docx); using WordprocessingDocument doc = WordprocessingDocument.Open(stream, false); MainDocumentPart main = doc.MainDocumentPart!; int drawings = main.Document.Body!.Descendants().Count(); string texte = string.Concat(main.Document.Body.Descendants().Select(t => t.Text)); var extents = main.Document.Body.Descendants() .Select(e => $"{e.Cx!.Value}x{e.Cy!.Value}") .ToList(); return (imageParts, drawings, texte, extents); } // ------------------------------------------------------------------ // Tests // ------------------------------------------------------------------ [Fact] public void ChpImageSimple_Base64Png_InsereDrawingEtImagePart() { string base64 = Convert.ToBase64String(FakePng(256, 100)); byte[] docx = CreerDocx("Signature: $UNCR.IMAGESIGNATURE$"); var champs = new[] { ChampImage("$UNCR.IMAGESIGNATURE$", base64, "CR_Signature") }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, champs); var (imageParts, drawings, texte, _) = Inspecter(resultat); Assert.Equal(1, imageParts); Assert.Equal(1, drawings); Assert.Contains("Signature: ", texte); Assert.DoesNotContain("$UNCR.IMAGESIGNATURE$", texte); Assert.DoesNotContain(base64, texte); // aucun Base64 visible } [Fact] public void ChpImageSimple_DeuxSignatures_DeuxImagesDistinctes() { string signature1 = Convert.ToBase64String(FakePng(256, 100)); string signature2 = Convert.ToBase64String(FakePng(512, 200)); byte[] docx = CreerDocx("S1: $UNCR.IMAGESIGNATURE$", "S2: $UNCR.IMAGESIGNATURETECH$"); var champs = new[] { ChampImage("$UNCR.IMAGESIGNATURE$", signature1, "CR_Signature"), ChampImage("$UNCR.IMAGESIGNATURETECH$", signature2, "CR_SignatureTech") }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, champs); var (imageParts, drawings, texte, _) = Inspecter(resultat); Assert.Equal(2, imageParts); Assert.Equal(2, drawings); Assert.DoesNotContain(signature1, texte); Assert.DoesNotContain(signature2, texte); } [Fact] public void ChpImageSimple_SignatureVide_PlaceholderSupprime() { byte[] docx = CreerDocx("Signature: $UNCR.IMAGESIGNATURE$"); var champs = new[] { ChampImage("$UNCR.IMAGESIGNATURE$", null, "CR_Signature") }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, champs); var (imageParts, drawings, texte, _) = Inspecter(resultat); Assert.Equal(0, imageParts); Assert.Equal(0, drawings); Assert.DoesNotContain("$UNCR.IMAGESIGNATURE$", texte); Assert.DoesNotContain("null", texte); } [Fact] public void ChpImageTableau_DeuxSignatures_DeuxImagesDansDeuxLignes() { string signature1 = Convert.ToBase64String(FakePng(256, 100)); string signature2 = Convert.ToBase64String(FakePng(512, 200)); byte[] docx = CreerDocxAvecTableau("$TABCR.IMAGESIGNATURE$"); string json = JsonSerializer.Serialize(new[] { new { signature = signature1 }, new { signature = signature2 } }); var tableau = new RapportChampDto { Id = "1", Rubrique = "$TABCOMPTERENDU$", Type = "Tableau", ListeDonnesBD = json, ListeMembre = new[] { new RapportChampSousElementDto { Rubrique = "$TABCR.IMAGESIGNATURE$", MembreMob = "CR_Signature", Type = "ChpImage" } } }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, new[] { tableau }); var (imageParts, drawings, texte, _) = Inspecter(resultat); Assert.Equal(2, imageParts); Assert.Equal(2, drawings); Assert.DoesNotContain("$TABCR.IMAGESIGNATURE$", texte); Assert.DoesNotContain(signature1, texte); Assert.DoesNotContain(signature2, texte); } [Fact] public void ChpImageSimple_Unphoto_InsereImage() { string base64 = Convert.ToBase64String(FakePng(320, 240)); byte[] docx = CreerDocx("Photo: $UNPHOTO.IMAGE$"); var champs = new[] { ChampImage("$UNPHOTO.IMAGE$", base64, "saNom") }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, champs); var (imageParts, drawings, texte, _) = Inspecter(resultat); Assert.Equal(1, imageParts); Assert.Equal(1, drawings); Assert.DoesNotContain("$UNPHOTO.IMAGE$", texte); } [Fact] public void ChpImageTableau_DeuxPhotos_DeuxImages() { string photo1 = Convert.ToBase64String(FakePng(320, 240)); string photo2 = Convert.ToBase64String(FakePng(640, 480)); byte[] docx = CreerDocxAvecTableau("$TABPHOTO.IMAGE$"); string json = JsonSerializer.Serialize(new[] { new { nom = photo1 }, new { nom = photo2 } }); var tableau = new RapportChampDto { Id = "1", Rubrique = "$TABPHOTO$", Type = "Tableau", ListeDonnesBD = json, ListeMembre = new[] { new RapportChampSousElementDto { Rubrique = "$TABPHOTO.IMAGE$", MembreMob = "DC_Nom", Type = "ChpImage" } } }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, new[] { tableau }); var (imageParts, drawings, texte, _) = Inspecter(resultat); Assert.Equal(2, imageParts); Assert.Equal(2, drawings); Assert.DoesNotContain("$TABPHOTO.IMAGE$", texte); } [Fact] public void ChpImageTableau_ImageDateNote_SurBonneLigne() { string photo1 = Convert.ToBase64String(FakePng(320, 240)); string photo2 = Convert.ToBase64String(FakePng(640, 480)); byte[] docx = CreerDocxAvecTableau("$TABPHOTO.IMAGE$", "$TABPHOTO.DATEIMAGE$", "$TABPHOTO.NOTEIMAGE$"); string json = JsonSerializer.Serialize(new[] { new { nom = photo1, date = "2024-01-01", notes = "Note A" }, new { nom = photo2, date = "2024-01-02", notes = "Note B" } }); var tableau = new RapportChampDto { Id = "1", Rubrique = "$TABPHOTO$", Type = "Tableau", ListeDonnesBD = json, ListeMembre = new[] { new RapportChampSousElementDto { Rubrique = "$TABPHOTO.IMAGE$", MembreMob = "DC_Nom", Type = "ChpImage" }, new RapportChampSousElementDto { Rubrique = "$TABPHOTO.DATEIMAGE$", MembreMob = "saDate" }, new RapportChampSousElementDto { Rubrique = "$TABPHOTO.NOTEIMAGE$", MembreMob = "saNotes" } } }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, new[] { tableau }); var (imageParts, drawings, texte, _) = Inspecter(resultat); Assert.Equal(2, imageParts); Assert.Equal(2, drawings); Assert.Contains("2024-01-01", texte); Assert.Contains("2024-01-02", texte); Assert.Contains("Note A", texte); Assert.Contains("Note B", texte); Assert.DoesNotContain("$TABPHOTO", texte); } [Fact] public void ChpImage_Base64Invalide_PlaceholderSupprimeSansCorruption() { byte[] docx = CreerDocx("Signature: $UNCR.IMAGESIGNATURE$"); var champs = new[] { ChampImage("$UNCR.IMAGESIGNATURE$", "###pas-du-base64###", "CR_Signature") }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, champs); var (imageParts, drawings, texte, _) = Inspecter(resultat); Assert.Equal(0, imageParts); Assert.Equal(0, drawings); Assert.DoesNotContain("$UNCR.IMAGESIGNATURE$", texte); Assert.DoesNotContain("pas-du-base64", texte); } [Fact] public void ChpImage_RatioImage_Conserve() { string base64 = Convert.ToBase64String(FakePng(256, 100)); // ratio 2.56 byte[] docx = CreerDocx("$UNCR.IMAGESIGNATURE$"); var champs = new[] { ChampImage("$UNCR.IMAGESIGNATURE$", base64, "CR_Signature") }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, champs); var (_, _, _, extents) = Inspecter(resultat); string extent = Assert.Single(extents); string[] parts = extent.Split('x'); long cx = long.Parse(parts[0]); long cy = long.Parse(parts[1]); // Largeur bornée à 54,9 mm ; hauteur proportionnelle au ratio 100/256. Assert.Equal(1976400L, cx); Assert.Equal((long)(1976400L * 100.0 / 256.0), cy); } [Fact] public void ChpImage_ChampTexte_NonRegression() { byte[] docx = CreerDocx("Réf: $REFINTERNE$"); var champs = new[] { new RapportChampDto { Id = "1", Rubrique = "$REFINTERNE$", Type = "ChpSimpleChaine", ListeDonnesBD = "ABC-123" } }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, champs); var (imageParts, drawings, texte, _) = Inspecter(resultat); Assert.Equal(0, imageParts); Assert.Equal(0, drawings); Assert.Contains("ABC-123", texte); Assert.DoesNotContain("$REFINTERNE$", texte); } [Fact] public void ChpImage_ImagePart_PlaceSousWordMedia() { // La photo doit être placée sous /word/media/ avec une cible RELATIVE : // un ImagePart à la racine (/media/…) avec une cible absolue (/media/…) // est invisible à l'export PDF de LibreOffice (photo absente du PDF). // Le SDK OpenXML 3.x (issue #1730) place les images à la racine ; le // moteur doit donc insérer l'image manuellement sous /word/media/. string base64 = Convert.ToBase64String(FakePng(256, 100)); byte[] docx = CreerDocx("$UNPHOTO.IMAGE$"); var champs = new[] { ChampImage("$UNPHOTO.IMAGE$", base64, "saNom") }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, champs); using var pkgStream = new MemoryStream(resultat); using Package package = Package.Open(pkgStream, FileMode.Open, FileAccess.Read); var mediaParts = package.GetParts() .Where(p => p.Uri.OriginalString.StartsWith("/word/media/", StringComparison.Ordinal)) .ToList(); Assert.Single(mediaParts); Assert.StartsWith("/word/media/", mediaParts[0].Uri.OriginalString, StringComparison.Ordinal); // La cible de la relation image est RELATIVE (jamais /media/… absolue). var imageRelations = package .GetPart(new Uri("/word/document.xml", UriKind.Relative)) .GetRelationshipsByType("http://schemas.openxmlformats.org/officeDocument/2006/relationships/image") .ToList(); Assert.Single(imageRelations); Assert.False(imageRelations[0].TargetUri.OriginalString.StartsWith("/", StringComparison.Ordinal)); Assert.StartsWith("media/", imageRelations[0].TargetUri.OriginalString, StringComparison.Ordinal); } [Fact] public void RemplirRubriques_DateHeureEtDate_FormatesSelonTypeEtMasque() { // §9/§22 : la même valeur source (31/08/2022 08:00) produit deux rendus. byte[] docx = CreerDocx("Début: $UNRDV.DATEDEBUT$ / Date: $UNRDV.DATEDEBUT_D$"); var champs = new[] { new RapportChampDto { Id = "1", Rubrique = "$UNRDV.DATEDEBUT$", Type = "ChpSimpleDateHeure", MasqueSaisie = "JJ/MM/AAAA HH:mm", ListeDonnesBD = "31/08/2022 08:00:00" }, new RapportChampDto { Id = "2", Rubrique = "$UNRDV.DATEDEBUT_D$", Type = "ChpSimpleDate", MasqueSaisie = "JJ/MM/AAAA", ListeDonnesBD = "31/08/2022 08:00:00" } }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, champs); var (_, _, texte, _) = Inspecter(resultat); Assert.Contains("31/08/2022 08:00", texte); Assert.Contains("31/08/2022", texte); Assert.DoesNotContain("31/08/2022 08:00:00", texte); // jamais la valeur brute Assert.DoesNotContain("$UNRDV", texte); } [Fact] public void Batik_MotClefTexte_NonRegression() { // Le moteur partagé doit continuer à substituer les mot-clefs Batik // () en texte, sans jamais les traiter comme des images. byte[] docx = CreerDocx("Client: "); var champs = new[] { new RapportChampDto { Id = "1", Rubrique = "", Type = "ChpSimpleChaine", ListeDonnesBD = "Acme" } }; byte[] resultat = RapportPdfService.RemplirRubriques(docx, champs); var (imageParts, drawings, texte, _) = Inspecter(resultat); Assert.Equal(0, imageParts); Assert.Equal(0, drawings); Assert.Contains("Acme", texte); Assert.DoesNotContain("", texte); } }