You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

511 lines
26KB

  1. using DocumentFormat.OpenXml;
  2. using DocumentFormat.OpenXml.Packaging;
  3. using DocumentFormat.OpenXml.Spreadsheet;
  4. using DocumentFormat.OpenXml.Wordprocessing;
  5. using System.Text;
  6. using System.Text.RegularExpressions;
  7. using System.Xml.Linq;
  8. using System.IO;
  9. using A = DocumentFormat.OpenXml.Drawing;
  10. using DW = DocumentFormat.OpenXml.Drawing.Wordprocessing;
  11. using PIC = DocumentFormat.OpenXml.Drawing.Pictures;
  12. namespace ToolsServices
  13. {
  14. public static class DocxService
  15. {
  16. #region Méthodes publiques
  17. public static string ExtractDocxFromBytes(byte[] bytes)
  18. {
  19. using var ms = new MemoryStream(bytes);
  20. using var doc = WordprocessingDocument.Open(ms, false);
  21. var body = doc.MainDocumentPart.Document.Body;
  22. return body.InnerText;
  23. }
  24. public static string ExtractTextFromDocx(string fileFullname)
  25. {
  26. LoggerService.LogInfo($"DocxService.ExtractTextFromDocx : {fileFullname}");
  27. using var doc = WordprocessingDocument.Open(fileFullname, false);
  28. if (doc == null || doc.MainDocumentPart == null || doc.MainDocumentPart.Document == null)
  29. return string.Empty;
  30. return string.Join(" ", doc.MainDocumentPart.Document.Descendants<DocumentFormat.OpenXml.Wordprocessing.Text>().Select(t => t.Text));
  31. }
  32. public static string ExtractTextFromRtf(string fileFullname)
  33. {
  34. LoggerService.LogInfo($"DocxService.ExtractTextFromRtf : {fileFullname}");
  35. string rtfContent = File.ReadAllText(fileFullname);
  36. // Supprimer les balises RTF simples
  37. rtfContent = Regex.Replace(rtfContent, @"\\[a-z]+\d*", string.Empty);
  38. rtfContent = Regex.Replace(rtfContent, @"{\\[^}]+}", string.Empty);
  39. rtfContent = Regex.Replace(rtfContent, @"[{}]", string.Empty);
  40. rtfContent = Regex.Replace(rtfContent, @"\r\n|\n", " ");
  41. // Convertir les caractères encodés (\'xx) en Unicode
  42. rtfContent = Regex.Replace(rtfContent, @"\\'([0-9a-fA-F]{2})", match =>
  43. {
  44. int value = Convert.ToInt32(match.Groups[1].Value, 16);
  45. return Encoding.Default.GetString(new byte[] { (byte)value });
  46. });
  47. // Remplacer les sauts de ligne RTF
  48. rtfContent = rtfContent.Replace(@"\par", "\n").Replace(@"\tab", "\t");
  49. // Supprimer les balises RTF
  50. rtfContent = Regex.Replace(rtfContent, @"\\[a-zA-Z]+\d*", string.Empty);
  51. // Supprimer les groupes de styles inutiles
  52. rtfContent = Regex.Replace(rtfContent, @"{\\[^}]+}", string.Empty);
  53. // Supprimer les accolades et le reste des balises
  54. rtfContent = Regex.Replace(rtfContent, @"[{}]", string.Empty);
  55. // Nettoyage final
  56. return rtfContent.Trim();
  57. }
  58. public static bool Genere_Docx(string fullFilenameTemplate, string fullFilenameXML, string fullFilenameOutputDocx, string titreCV, int nbrMaxCompetences, bool isCVAnonyme)
  59. {
  60. LoggerService.LogInfo($"DocxService.Genere_Docx : {fullFilenameOutputDocx}");
  61. try
  62. {
  63. if (System.IO.File.Exists(fullFilenameOutputDocx))
  64. {
  65. System.IO.File.Delete(fullFilenameOutputDocx);
  66. }
  67. return CreateDocx(fullFilenameTemplate, fullFilenameXML, fullFilenameOutputDocx, titreCV, nbrMaxCompetences, isCVAnonyme);
  68. }
  69. catch (Exception ex)
  70. {
  71. LoggerService.LogError($"Erreur dans la génération d'un CV au format Word (DocxService.Genere_Docx) : {ex.Message}");
  72. return false;
  73. }
  74. }
  75. #endregion
  76. #region Propriétés privées
  77. private static string Font_Name = "Calibri";
  78. private static int Font_Size_int = 11;// "22"; // 11pt = 22 demi-points
  79. private static string Font_Size
  80. {
  81. get
  82. {
  83. return (Font_Size_int * 2).ToString(); ;
  84. }
  85. }
  86. private static string Font_Size_Minus
  87. {
  88. get
  89. {
  90. return ((Font_Size_int - 2) * 2).ToString(); ;
  91. }
  92. }
  93. #endregion
  94. #region Méthodes privées
  95. private static bool CreateDocx(string templatePath, string xmlPath, string outputPath, string _TitreCV, int _NbrMaxCompetences, bool isCVAnonyme)
  96. {
  97. LoggerService.LogInfo($"DocxService.CreateDocx : {templatePath}");
  98. try
  99. {
  100. if (File.Exists(outputPath))
  101. File.Delete(outputPath);
  102. File.Copy(templatePath, outputPath, true); // Copie le modèle
  103. #region Lire le XML
  104. var cv = XDocument.Load(xmlPath).Root;
  105. var placeholders = new Dictionary<string, string>();
  106. #region Identité
  107. var identite = cv!.Element("Identite");
  108. if(isCVAnonyme)
  109. {
  110. var trigramme = "AAA";
  111. if (identite!.Element("Prenom")?.Value.Length > 1)
  112. {
  113. trigramme = identite!.Element("Prenom")?.Value.Substring(0, 1).ToUpper();
  114. }
  115. if (identite!.Element("Nom")?.Value.Length > 2)
  116. {
  117. trigramme += identite!.Element("Nom")?.Value.Substring(0, 2).ToUpper();
  118. }
  119. placeholders["{{Nom}}"] = trigramme!;
  120. placeholders["{{Prenom}}"] = "";
  121. }
  122. else
  123. {
  124. placeholders["{{Nom}}"] = identite!.Element("Nom")?.Value ?? "";
  125. placeholders["{{Prenom}}"] = identite!.Element("Prenom")?.Value ?? "";
  126. }
  127. placeholders["{{Email}}"] = identite!.Element("Email")?.Value ?? "";
  128. placeholders["{{Telephone}}"] = identite!.Element("Telephone")?.Value ?? "";
  129. #endregion
  130. #region profil
  131. // Profil : première phrase en gras, le reste plus petit sur lignes séparées
  132. string profil = cv.Element("Profil")?.Value ?? "";
  133. string profilAccroche = "";
  134. string profilDetails = "";
  135. var phrases = profil.Split('.').Select(p => p.Trim()).Where(p => !string.IsNullOrEmpty(p)).ToList();
  136. if (phrases.Count > 0)
  137. {
  138. if (_TitreCV != "")
  139. {
  140. profilAccroche = _TitreCV;
  141. profilDetails = string.Join("\n", phrases.Select(p => p));
  142. }
  143. else
  144. {
  145. profilAccroche = phrases[0];
  146. profilDetails = string.Join("\n", phrases.Skip(1).Select(p => p));
  147. }
  148. }
  149. #endregion
  150. #region Langues
  151. var langues = cv.Element("Langues")?.Elements("Langue") ?? Enumerable.Empty<XElement>();
  152. var languesTextList = langues.Select(l => $"{l.Attribute("nom")?.Value} ({l.Attribute("niveau")?.Value})").ToList();
  153. string languesText = string.Join(", ", languesTextList);
  154. #endregion
  155. #region Formations
  156. var formations = cv.Element("Formations")?.Elements("Formation") ?? Enumerable.Empty<XElement>();
  157. var formationsTextList = formations.Select(f => $"{f.Element("Diplome")?.Value} à {f.Element("Etablissement")?.Value} ({f.Element("DateFin")?.Value})").ToList();
  158. string formationsText = string.Join("\n", formationsTextList);
  159. #endregion
  160. #region Certifications
  161. var certifications = cv.Element("Certifications")?.Elements("Certification") ?? Enumerable.Empty<XElement>();
  162. var certificationsTextList = formations.Select(f => $"{f.Element("Nom")?.Value} à {f.Element("Organisme")?.Value} ({f.Element("Date")?.Value})").ToList();
  163. //var certificationsTextList = formations.Select(f => $"{f.Element("Nom")?.Value}").ToList();
  164. string certificationsText = string.Join("\n", certificationsTextList);
  165. #endregion
  166. #region Compétences : x premières
  167. var competences = cv.Element("Competences")?
  168. .Elements("Competence")
  169. .Select(c => c.Attribute("nom")?.Value)
  170. .Where(n => !string.IsNullOrEmpty(n))
  171. .Take(_NbrMaxCompetences)
  172. .ToList() ?? new List<string>()!;
  173. string competencesText = string.Join(", ", competences);
  174. #endregion
  175. #region Expériences : chaque bloc avec première ligne en gras
  176. var experiences = cv.Element("Experiences")?.Elements("Experience") ?? Enumerable.Empty<XElement>();
  177. var xpParagraphs = new List<Paragraph>();
  178. foreach (var xp in experiences)
  179. {
  180. string datefin = xp.Element("DateFin")?.Value! != "" ? xp.Element("DateFin")?.Value! : "Aujourd’hui";
  181. string titre = $"{xp.Element("Poste")?.Value} chez {xp.Element("Entreprise")?.Value} à {xp.Element("Lieu")?.Value}";
  182. string dates = $"{xp.Element("DateDebut")?.Value} à {datefin}";
  183. string desc = xp.Element("Description")?.Value ?? "";
  184. var paraTitre = new Paragraph(new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Text(titre)));
  185. paraTitre.GetFirstChild<DocumentFormat.OpenXml.Wordprocessing.Run>()!.RunProperties = new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
  186. new RunFonts() { Ascii = Font_Name, HighAnsi = Font_Name },
  187. new DocumentFormat.OpenXml.Wordprocessing.FontSize() { Val = Font_Size },
  188. new DocumentFormat.OpenXml.Wordprocessing.Bold()
  189. );
  190. var runDates = new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Text(dates));
  191. runDates.RunProperties = new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
  192. new RunFonts() { Ascii = Font_Name, HighAnsi = Font_Name },
  193. new DocumentFormat.OpenXml.Wordprocessing.FontSize() { Val = Font_Size },
  194. new DocumentFormat.OpenXml.Wordprocessing.Italic()
  195. );
  196. var paraDates = new Paragraph(runDates);
  197. var runDesc = new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Text(desc));
  198. runDesc.RunProperties = new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
  199. new RunFonts() { Ascii = Font_Name, HighAnsi = Font_Name },
  200. new DocumentFormat.OpenXml.Wordprocessing.FontSize() { Val = Font_Size_Minus }
  201. );
  202. var paraDesc = new Paragraph(runDesc);
  203. xpParagraphs.Add(paraTitre);
  204. xpParagraphs.Add(paraDates);
  205. xpParagraphs.Add(paraDesc);
  206. xpParagraphs.Add(new Paragraph(new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Text("")))); // Ligne vide entre les expériences
  207. }
  208. #endregion
  209. #endregion
  210. #region Ecrire dans le document
  211. using (var doc = WordprocessingDocument.Open(outputPath, true))
  212. {
  213. var body = doc!.MainDocumentPart!.Document.Body;
  214. #region Ajouter un style de puces s’il n'existe pas
  215. var numberingPart = doc!.MainDocumentPart!.NumberingDefinitionsPart;
  216. if (numberingPart == null)
  217. numberingPart = doc.MainDocumentPart.AddNewPart<NumberingDefinitionsPart>();
  218. numberingPart.Numbering = new Numbering(
  219. new AbstractNum(
  220. new Level(
  221. new DocumentFormat.OpenXml.Wordprocessing.NumberingFormat() { Val = NumberFormatValues.Bullet },
  222. new LevelText() { Val = "•" },
  223. new LevelJustification() { Val = LevelJustificationValues.Left }
  224. )
  225. { LevelIndex = 0 }
  226. )
  227. { AbstractNumberId = 1 },
  228. new NumberingInstance(
  229. new AbstractNumId() { Val = 1 }
  230. )
  231. { NumberID = 1 }
  232. );
  233. #endregion
  234. #region Profil (mise en forme particulière)
  235. foreach (var para in body!.Descendants<Paragraph>())
  236. {
  237. if (para.InnerText.Contains("{{Profil}}"))
  238. {
  239. para.RemoveAllChildren<DocumentFormat.OpenXml.Wordprocessing.Run>();
  240. var runBold = new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Text(profilAccroche));
  241. runBold.RunProperties = new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
  242. new RunFonts() { Ascii = Font_Name, HighAnsi = Font_Name },
  243. new DocumentFormat.OpenXml.Wordprocessing.FontSize() { Val = "24" },
  244. new DocumentFormat.OpenXml.Wordprocessing.Bold()
  245. );
  246. para.Append(runBold);
  247. foreach (var line in profilDetails.Split('\n'))
  248. {
  249. var run = new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Text(line));
  250. var smallPara = new Paragraph(run);
  251. var props = new DocumentFormat.OpenXml.Wordprocessing.RunProperties(new RunFonts() { Ascii = Font_Name, HighAnsi = Font_Name }, new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "18" }); // 9pt
  252. smallPara!.GetFirstChild<DocumentFormat.OpenXml.Wordprocessing.Run>()!.RunProperties = props;
  253. smallPara!.ParagraphProperties = new ParagraphProperties(new Justification { Val = JustificationValues.Center });
  254. para.InsertAfterSelf(smallPara);
  255. }
  256. break;
  257. }
  258. }
  259. #endregion
  260. #region Experiences
  261. foreach (var para in body.Descendants<Paragraph>().ToList())
  262. {
  263. if (para.InnerText.Contains("{{Experiences}}"))
  264. {
  265. var parent = para.Parent;
  266. foreach (var xpPara in xpParagraphs)
  267. {
  268. parent!.InsertBefore(xpPara.CloneNode(true), para);
  269. }
  270. para.Remove();
  271. break;
  272. }
  273. }
  274. #endregion
  275. #region Langues
  276. if (body.InnerText.Contains("{{Langues}}"))
  277. {
  278. var paraToReplace = body.Descendants<Paragraph>()
  279. .FirstOrDefault(p => p.InnerText.Contains("{{Langues}}"));
  280. if (paraToReplace != null)
  281. {
  282. var parent = paraToReplace.Parent;
  283. var lstLangues = cv.Element("Langues")?.Elements("Langue")
  284. .Select(l =>
  285. {
  286. string sReturn = "";
  287. string nom = l.Attribute("nom")?.Value ?? "";
  288. string niveau = l.Attribute("niveau")?.Value ?? "";
  289. sReturn = $"{nom}";
  290. if (niveau != null && niveau != "") sReturn += $" ({niveau})";
  291. return sReturn;
  292. })
  293. .ToList() ?? new List<string>();
  294. foreach (var langue in lstLangues)
  295. {
  296. var run = new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Text(langue));
  297. run.RunProperties = new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
  298. new RunFonts() { Ascii = Font_Name, HighAnsi = Font_Name },
  299. new DocumentFormat.OpenXml.Wordprocessing.FontSize() { Val = Font_Size } // 11pt = 22 demi-points
  300. );
  301. var para = new Paragraph(
  302. new ParagraphProperties(
  303. new NumberingProperties(
  304. new NumberingLevelReference() { Val = 0 },
  305. new NumberingId() { Val = 1 })
  306. ),
  307. run
  308. );
  309. parent!.InsertAfter(para, paraToReplace);
  310. }
  311. paraToReplace.Remove();
  312. }
  313. }
  314. #endregion
  315. #region Formations
  316. if (body.InnerText.Contains("{{Formations}}"))
  317. {
  318. var paraToReplace = body.Descendants<Paragraph>()
  319. .FirstOrDefault(p => p.InnerText.Contains("{{Formations}}"));
  320. if (paraToReplace != null)
  321. {
  322. var parent = paraToReplace.Parent;
  323. var lstFormations = cv.Element("Formations")?.Elements("Formation")
  324. .Select(f =>
  325. {
  326. string sReturn = "";
  327. string intitule = f.Element("Diplome")?.Value ?? "";
  328. string lieu = f.Element("Etablissement")?.Value ?? "";
  329. string annee = f.Element("DateFin")?.Value ?? "";
  330. sReturn = $"{intitule}";
  331. if (lieu != null && lieu != "") sReturn += $" - {lieu}";
  332. if (annee != null && annee != "") sReturn += $" ({annee})";
  333. return sReturn;// $"{intitule} - {lieu} ({annee})";
  334. })
  335. .ToList() ?? new List<string>();
  336. foreach (var formation in lstFormations)
  337. {
  338. var run = new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Text(formation));
  339. run.RunProperties = new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
  340. new RunFonts() { Ascii = Font_Name, HighAnsi = Font_Name },
  341. new DocumentFormat.OpenXml.Wordprocessing.FontSize() { Val = Font_Size } // 11pt = 22 demi-points
  342. );
  343. var para = new Paragraph(
  344. new ParagraphProperties(
  345. new NumberingProperties(
  346. new NumberingLevelReference() { Val = 0 },
  347. new NumberingId() { Val = 1 })
  348. ),
  349. run
  350. );
  351. parent!.InsertAfter(para, paraToReplace);
  352. }
  353. paraToReplace.Remove();
  354. }
  355. }
  356. #endregion
  357. #region Certifications
  358. if (body.InnerText.Contains("{{Certifications}}"))
  359. {
  360. var paraToReplace = body.Descendants<Paragraph>()
  361. .FirstOrDefault(p => p.InnerText.Contains("{{Certifications}}"));
  362. if (paraToReplace != null)
  363. {
  364. var parent = paraToReplace.Parent;
  365. var lstCertifications = cv.Element("Certifications")?.Elements("Certification")
  366. .Select(f =>
  367. {
  368. string sReturn = "";
  369. string intitule = f.Element("Nom")?.Value ?? "";
  370. string lieu = f.Element("Organisme")?.Value ?? "";
  371. string annee = f.Element("Date")?.Value ?? "";
  372. sReturn = $"{intitule}";
  373. if (lieu != null && lieu != "") sReturn += $" - {lieu}";
  374. if (annee != null && annee != "") sReturn += $" ({annee})";
  375. return sReturn;
  376. })
  377. .ToList() ?? new List<string>();
  378. foreach (var certification in lstCertifications)
  379. {
  380. var run = new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Text(certification));
  381. run.RunProperties = new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
  382. new RunFonts() { Ascii = Font_Name, HighAnsi = Font_Name },
  383. new DocumentFormat.OpenXml.Wordprocessing.FontSize() { Val = Font_Size } // 11pt = 22 demi-points
  384. );
  385. var para = new Paragraph(
  386. new ParagraphProperties(
  387. new NumberingProperties(
  388. new NumberingLevelReference() { Val = 0 },
  389. new NumberingId() { Val = 1 })
  390. ),
  391. run
  392. );
  393. parent!.InsertAfter(para, paraToReplace);
  394. }
  395. paraToReplace.Remove();
  396. }
  397. }
  398. #endregion
  399. #region Le reste
  400. foreach (var para in body!.Descendants<Paragraph>())
  401. {
  402. foreach (var run in para.Descendants<DocumentFormat.OpenXml.Wordprocessing.Run>())
  403. {
  404. /*
  405. run.RunProperties = new RunProperties(
  406. new RunFonts() { Ascii = Font_Name, HighAnsi = Font_Name },
  407. new FontSize() { Val = Font_Size });
  408. */
  409. var text = run.GetFirstChild<DocumentFormat.OpenXml.Wordprocessing.Text>();
  410. if (text == null) continue;
  411. if (text.Text.Contains("{{Nom}}")) text.Text = text.Text.Replace("{{Nom}}", placeholders["{{Nom}}"]);
  412. if (text.Text.Contains("{{Prenom}}")) text.Text = text.Text.Replace("{{Prenom}}", placeholders["{{Prenom}}"]);
  413. if (text.Text.Contains("{{Telephone}}")) text.Text = text.Text.Replace("{{Telephone}}", placeholders["{{Telephone}}"]);
  414. if (text.Text.Contains("{{Email}}")) text.Text = text.Text.Replace("{{Email}}", placeholders["{{Email}}"]);
  415. if (text.Text.Contains("{{Competences}}")) text.Text = text.Text.Replace("{{Competences}}", competencesText);
  416. //if (text.Text.Contains("{{Formations}}")) text.Text = text.Text.Replace("{{Formations}}", formationsText);
  417. //if (text.Text.Contains("{{Langues}}")) text.Text = text.Text.Replace("{{Langues}}", languesText);
  418. }
  419. }
  420. #endregion
  421. doc.MainDocumentPart.Document.Save();
  422. }
  423. #endregion
  424. return true;
  425. }
  426. catch (Exception ex)
  427. {
  428. LoggerService.LogError($"Erreur dans la génération d'un CV au format Word (DocxService.CreateDocx) : {ex.Message}");
  429. return false;
  430. }
  431. }
  432. #endregion
  433. }
  434. }