Files
Omika/tests/Omika.Tests/Services/RapportPdfServiceImageTests.cs
2026-10-23 18:46:50 +02:00

410 lines
16 KiB
C#

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;
/// <summary>
/// Tests de l'insertion d'images ChpImage (Base64 / fichier) dans le DOCX par
/// <see cref="RapportPdfService"/> : Drawing OpenXML + ImagePart, suppression du
/// placeholder, non-régression des champs texte, conservation du ratio.
/// </summary>
public class RapportPdfServiceImageTests
{
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
/// <summary>Construit des octets « PNG-like » (magic bytes + dimensions) pour les tests DOCX.</summary>
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<string> 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<Drawing>().Count();
string texte = string.Concat(main.Document.Body.Descendants<Text>().Select(t => t.Text));
var extents = main.Document.Body.Descendants<WP.Extent>()
.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
// (<CLIENT.NOM>) en texte, sans jamais les traiter comme des images.
byte[] docx = CreerDocx("Client: <CLIENT.NOM>");
var champs = new[]
{
new RapportChampDto { Id = "1", Rubrique = "<CLIENT.NOM>", 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("<CLIENT.NOM>", texte);
}
}