From 4b3cc8305b26d1dfa71e0752104f1de4efdc7423 Mon Sep 17 00:00:00 2001 From: Victor Rudolfsson Date: Tue, 15 Oct 2024 11:08:48 +1000 Subject: [PATCH 1/5] feat: Zavy Conversion Endpoint --- .github/workflows/publish.yml | 33 +++ Dockerfile | 2 + .../Controllers/ZavyController.cs | 276 ++++++++++++++++++ src/ej2-documenteditor-server/NuGet.Config | 2 +- .../ej2-documenteditor-server.csproj | 6 + 5 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish.yml create mode 100644 src/ej2-documenteditor-server/Controllers/ZavyController.cs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..210703a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +name: Build & Deploy SyncFusion Backend Image to AWS +on: + push: + branches: + - main + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: ⬇️ Checkout repository ⬇️ + uses: actions/checkout@v4 + + - name: ☁️ Configure AWS ☁️ + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_DEPLOY_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_DEPLOY_SECRET_ACCESS_KEY }} + aws-region: ap-southeast-2 + - name: Expose GitHub Runtime + uses: crazy-max/ghaction-github-runtime@v3 + - name: 🐧 Set up QEMU 🐧 + uses: docker/setup-qemu-action@v2 + - name: 🐋 Log in to Amazon ECR 🐋 + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + - name: ⚓️ Docker BuildX ⚓️ + uses: docker/setup-buildx-action@v2 + - name: 🐳 docker build 🐳 + run: docker build -t 077004640279.dkr.ecr.ap-southeast-2.amazonaws.com . + - name: 🚀 docker push 🚀 + run: docker push 077004640279.dkr.ecr.ap-southeast-2.amazonaws.com/syncfusion:latest \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index bd9f44a..3a7dd03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ ENV SYNCFUSION_LICENSE_KEY="" ENV SPELLCHECK_DICTIONARY_PATH="" ENV SPELLCHECK_JSON_FILENAME="" ENV SPELLCHECK_CACHE_COUNT="" +RUN apt-get update \ + && apt-get install -y libfontconfig FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /source diff --git a/src/ej2-documenteditor-server/Controllers/ZavyController.cs b/src/ej2-documenteditor-server/Controllers/ZavyController.cs new file mode 100644 index 0000000..51bcd30 --- /dev/null +++ b/src/ej2-documenteditor-server/Controllers/ZavyController.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using System.Net.Http; +using System.IO; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Syncfusion.EJ2.DocumentEditor; +using System.Text; +using WDocument = Syncfusion.DocIO.DLS.WordDocument; +using WFormatType = Syncfusion.DocIO.FormatType; +using Syncfusion.EJ2.SpellChecker; +using Syncfusion.DocIORenderer; +using Syncfusion.Pdf; + +namespace EJ2DocumentEditorServer.Controllers +{ + [Route("api/[controller]")] + public class ZavyController : Controller + { + private readonly IHostingEnvironment _hostingEnvironment; + string path; + public ZavyController(IHostingEnvironment hostingEnvironment) + { + _hostingEnvironment = hostingEnvironment; + path = Startup.path; + } + + //Converts Metafile to raster image. + private static void OnMetafileImageParsed(object sender, MetafileImageParsedEventArgs args) + { + //You can write your own method definition for converting metafile to raster image using any third-party image converter. + args.ImageStream = ConvertMetafileToRasterImage(args.MetafileStream); + } + + private static Stream ConvertMetafileToRasterImage(Stream ImageStream) + { + //Here we are loading a default raster image as fallback. + Stream imgStream = GetManifestResourceStream("ImageNotFound.jpg"); + return imgStream; + //To do : Write your own logic for converting metafile to raster image using any third-party image converter(Syncfusion doesn't provide any image converter). + } + + private static Stream ConvertMetafileToPng(Stream ImageStream) + { + //Here we are loading a default raster image as fallback. + Stream imgStream = GetManifestResourceStream("ImageNotFound.jpg"); + return imgStream; + //To do : Write your own logic for converting metafile to raster image using any third-party image converter(Syncfusion doesn't provide any image converter). + } + + private static Stream GetManifestResourceStream(string fileName) + { + System.Reflection.Assembly execAssembly = typeof(WDocument).Assembly; + string[] resourceNames = execAssembly.GetManifestResourceNames(); + foreach (string resourceName in resourceNames) + { + if (resourceName.EndsWith("." + fileName)) + { + fileName = resourceName; + break; + } + } + return execAssembly.GetManifestResourceStream(fileName); + } + + + // JSON parameters accepted by the endpoint + public class ConversionParameters + { + // Current format (this can be auto-detected and is not required): + public string from { get; set; } + + // Export format: + public string export { get; set; } + + // Value to convert. Will be automatically decoded if base64 encoded: + public string content { get; set; } + } + + [AcceptVerbs("Post")] + [HttpPost] + [EnableCors("AllowAllOrigins")] + [Route("conversion")] + public string Conversion([FromBody]ConversionParameters param) + { + if (string.IsNullOrEmpty(param.content)) + { + throw new NotSupportedException("Content is empty."); + } + if (string.IsNullOrEmpty(param.export)) + { + throw new NotSupportedException("Export format is empty."); + } + + + try + { + // If we need to export to sfdt, we need to convert the content to WordDocument (not WDocument from DocIO) + if (DotPrefix(param.export) == ".sfdt") + { + WordDocument document = this.GetDocument(param); + string json = Newtonsoft.Json.JsonConvert.SerializeObject(document); + document.Dispose(); + return json; + } + else if (DotPrefix(param.export) == ".pdf") + { + MemoryStream stream = new MemoryStream(); + WDocument document = this.GetWDocument(param); + document.Save(stream, WFormatType.Docx); + string base64 = this.ExportPDF(document); + document.Dispose(); + return base64; + } + else + { + WDocument document = this.GetWDocument(param); + MemoryStream stream = new MemoryStream(); + document.Save(stream, GetWFormatType(param.export)); + document.Dispose(); + stream.Position = 0; + return Convert.ToBase64String(stream.ToArray()); + } + } + catch (Exception e) + { + return "Oh no! " + e.Message + " format: " + param.from + " export: " + param.export; + } + } + + + // Helpers: + + // Normalize the format string to start with a dot + // since that's what Syncfusion expects by default + internal static string DotPrefix(string format) + { + if (format.StartsWith(".")) return format.ToLower(); + return "." + format.ToLower(); + } + + // Decode base64 if it's base64, otherwise just returns the + // string as it is + internal static string DecodeIfBase64(string base64) + { + if (IsBase64(base64)) { + return Encoding.UTF8.GetString(Convert.FromBase64String(base64)); + } + return base64; + } + + // Check if a string is base64 encoded + internal static bool IsBase64(string base64) + { + Span buffer = new Span(new byte[base64.Length]); + return Convert.TryFromBase64String(base64, buffer , out int bytesParsed); + } + + // Gets the FormatType from an extension string + internal static FormatType GetFormatType(string format) + { + if (string.IsNullOrEmpty(format)) + throw new NotSupportedException("EJ2 DocumentEditor does not support blank input format"); + + // Check if format starts with a ., otherwise add one: + switch (DotPrefix(format)) + { + case ".dotx": + case ".docx": + case ".docm": + case ".dotm": + return FormatType.Docx; + case ".dot": + case ".doc": + return FormatType.Doc; + case ".rtf": + return FormatType.Rtf; + case ".txt": + return FormatType.Txt; + case ".xml": + return FormatType.WordML; + case ".html": + return FormatType.Html; + default: + throw new NotSupportedException("EJ2 DocumentEditor does not support this input format (" + format + "). Supported formats: .docx, .doc, .rtf, .txt, .xml, .html and .pdf"); + } + } + internal static WFormatType GetWFormatType(string? format) + { + if (string.IsNullOrEmpty(format)) + return WFormatType.Automatic; + + // Check if format starts with a ., otherwise add one: + switch (DotPrefix(format.ToLower())) + { + case ".dotx": + return WFormatType.Dotx; + case ".docx": + return WFormatType.Docx; + case ".docm": + return WFormatType.Docm; + case ".dotm": + return WFormatType.Dotm; + case ".dot": + return WFormatType.Dot; + case ".doc": + return WFormatType.Doc; + case ".rtf": + return WFormatType.Rtf; + case ".txt": + return WFormatType.Txt; + case ".xml": + return WFormatType.WordML; + case ".odt": + return WFormatType.Odt; + case ".html": + return WFormatType.Html; + default: + throw new NotSupportedException("EJ2 DocumentEditor does not support this export format (" + format + "). Supported formats: .docx, .doc, .rtf, .txt, .xml, .odt, .html and .pdf"); + } + } + + private WDocument GetWDocument(ConversionParameters param) + { + byte[] byteArray = Encoding.UTF8.GetBytes(DecodeIfBase64(param.content)); + MemoryStream stream = new MemoryStream(byteArray); + + // Load the HTML content into the WordDocument + WDocument document = new WDocument(stream, GetWFormatType(param.from)); + stream.Dispose(); + + return document; + } + + private WordDocument GetDocument(ConversionParameters param) + { + //Hooks MetafileImageParsed event. + WordDocument.MetafileImageParsed += OnMetafileImageParsed; + WordDocument document = WordDocument.LoadString(DecodeIfBase64(param.content), GetFormatType(param.from)); + //Unhooks MetafileImageParsed event. + WordDocument.MetafileImageParsed -= OnMetafileImageParsed; + return document; + } + + + + internal string ExportPDF(WDocument wordDocument) + { + + // Instantiation of DocIORenderer for Word to PDF conversion. + DocIORenderer render = new DocIORenderer(); + + //Converts Word document into PDF document + PdfDocument pdfDocument = render.ConvertToPDF(wordDocument); + + //Dispose the resources. + render.Dispose(); + wordDocument.Dispose(); + + //Saves the PDF document to MemoryStream. + MemoryStream stream = new MemoryStream(); + pdfDocument.Save(stream); + stream.Position = 0; + //Dispose the PDF resources. + pdfDocument.Close(true); + PdfDocument.ClearFontCache(); + + //Return the PDF document. + return Convert.ToBase64String(stream.ToArray()); + } + } + +} diff --git a/src/ej2-documenteditor-server/NuGet.Config b/src/ej2-documenteditor-server/NuGet.Config index 8c27aca..c52a761 100644 --- a/src/ej2-documenteditor-server/NuGet.Config +++ b/src/ej2-documenteditor-server/NuGet.Config @@ -5,7 +5,7 @@ - + diff --git a/src/ej2-documenteditor-server/ej2-documenteditor-server.csproj b/src/ej2-documenteditor-server/ej2-documenteditor-server.csproj index 47e677f..640e7c8 100644 --- a/src/ej2-documenteditor-server/ej2-documenteditor-server.csproj +++ b/src/ej2-documenteditor-server/ej2-documenteditor-server.csproj @@ -47,6 +47,12 @@ + + + + + + From f7540ea17a85a0ba1801b5fcc0a33796e080d399 Mon Sep 17 00:00:00 2001 From: Victor Rudolfsson Date: Tue, 15 Oct 2024 11:43:50 +1000 Subject: [PATCH 2/5] hotfix: deploy branch --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 210703a..a834356 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,7 +2,7 @@ name: Build & Deploy SyncFusion Backend Image to AWS on: push: branches: - - main + - master jobs: build-and-push: From 1689c724972a216506737819729a51b1887b0a63 Mon Sep 17 00:00:00 2001 From: Victor Rudolfsson Date: Tue, 15 Oct 2024 11:53:57 +1000 Subject: [PATCH 3/5] hotfix: deploy branch --- .github/workflows/publish.yml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a834356..a2448b6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout repository ⬇️ + - name: ⬇️ Checkout repository ⬇️ uses: actions/checkout@v4 - - name: ☁️ Configure AWS ☁️ + - name: ☁️ Configure AWS ☁️ uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_DEPLOY_ACCESS_KEY_ID }} @@ -20,14 +20,19 @@ jobs: aws-region: ap-southeast-2 - name: Expose GitHub Runtime uses: crazy-max/ghaction-github-runtime@v3 - - name: 🐧 Set up QEMU 🐧 + - name: 🐧 QEMU 🐧 uses: docker/setup-qemu-action@v2 - - name: 🐋 Log in to Amazon ECR 🐋 + - name: 🐋 Amazon ECR 🐋 id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - - name: ⚓️ Docker BuildX ⚓️ + - name: ⚓️ docker buildx setup ⚓️ uses: docker/setup-buildx-action@v2 - - name: 🐳 docker build 🐳 - run: docker build -t 077004640279.dkr.ecr.ap-southeast-2.amazonaws.com . - - name: 🚀 docker push 🚀 - run: docker push 077004640279.dkr.ecr.ap-southeast-2.amazonaws.com/syncfusion:latest \ No newline at end of file + - name: 🐳 docker build 🐳 + uses: docker/build-push-action@v3 + with: + context: . + cache-from: type=gha + cache-to: type=gha,mode=max + file: ./Dockerfile + push: true + tags: 077004640279.dkr.ecr.ap-southeast-2.amazonaws.com/syncfusion:latest \ No newline at end of file From 46dc79f0d047ae8c5151fdee09d6707bfd472cd6 Mon Sep 17 00:00:00 2001 From: Victor Rudolfsson Date: Tue, 15 Oct 2024 14:20:15 +1000 Subject: [PATCH 4/5] fix: default fonts --- Dockerfile | 5 +- .../Controllers/ZavyController.cs | 76 +++++++++++++++++-- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3a7dd03..67d8814 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,9 @@ ENV SYNCFUSION_LICENSE_KEY="" ENV SPELLCHECK_DICTIONARY_PATH="" ENV SPELLCHECK_JSON_FILENAME="" ENV SPELLCHECK_CACHE_COUNT="" -RUN apt-get update \ - && apt-get install -y libfontconfig +RUN echo "deb http://deb.debian.org/debian bullseye main contrib" > /etc/apt/sources.list \ + && apt-get update \ + && apt-get install -y libfontconfig ttf-mscorefonts-installer FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /source diff --git a/src/ej2-documenteditor-server/Controllers/ZavyController.cs b/src/ej2-documenteditor-server/Controllers/ZavyController.cs index 51bcd30..327a8dc 100644 --- a/src/ej2-documenteditor-server/Controllers/ZavyController.cs +++ b/src/ej2-documenteditor-server/Controllers/ZavyController.cs @@ -10,7 +10,12 @@ using Syncfusion.EJ2.DocumentEditor; using System.Text; using WDocument = Syncfusion.DocIO.DLS.WordDocument; +using WParagraph = Syncfusion.DocIO.DLS.WParagraph; +using WParagraphStyle = Syncfusion.DocIO.DLS.WParagraphStyle; +using WTextRange = Syncfusion.DocIO.DLS.WTextRange; using WFormatType = Syncfusion.DocIO.FormatType; +using Entity = Syncfusion.DocIO.DLS.Entity; +using EntityType = Syncfusion.DocIO.DLS.EntityType; using Syncfusion.EJ2.SpellChecker; using Syncfusion.DocIORenderer; using Syncfusion.Pdf; @@ -144,12 +149,15 @@ internal static string DotPrefix(string format) // Decode base64 if it's base64, otherwise just returns the // string as it is - internal static string DecodeIfBase64(string base64) + internal static byte[] DecodeIfBase64(string base64) { if (IsBase64(base64)) { - return Encoding.UTF8.GetString(Convert.FromBase64String(base64)); + return Convert.FromBase64String(base64); } - return base64; + if (IsValidUtf8(base64)) { + return Encoding.UTF8.GetBytes(base64); + }; + return Encoding.Default.GetBytes(base64); } // Check if a string is base64 encoded @@ -159,6 +167,22 @@ internal static bool IsBase64(string base64) return Convert.TryFromBase64String(base64, buffer , out int bytesParsed); } + + private static bool IsValidUtf8(string input) + { + try + { + // Try to decode the string as UTF-8 + Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(input)); + return true; + } + catch + { + // If an exception is thrown, the string is not valid UTF-8 + return false; + } + } + // Gets the FormatType from an extension string internal static FormatType GetFormatType(string format) { @@ -223,35 +247,73 @@ internal static WFormatType GetWFormatType(string? format) } } - private WDocument GetWDocument(ConversionParameters param) + public WDocument GetWDocument(ConversionParameters param) { - byte[] byteArray = Encoding.UTF8.GetBytes(DecodeIfBase64(param.content)); + byte[] byteArray = DecodeIfBase64(param.content); MemoryStream stream = new MemoryStream(byteArray); // Load the HTML content into the WordDocument WDocument document = new WDocument(stream, GetWFormatType(param.from)); + + // Set default font styles + WParagraphStyle defaultStyle = document.Styles.FindByName("Normal") as WParagraphStyle; + ChangeFontName(document, "Calibri"); + document.FontSettings.FallbackFonts.InitializeDefault(); stream.Dispose(); return document; } + static void ChangeFontName(WDocument document, string fontName) + { + // Find all paragraphs by EntityType in the Word document. + List paragraphs = document.FindAllItemsByProperty(EntityType.Paragraph, null, null); + + // Change the font name for all paragraph marks (non-printing characters). + for (int i = 0; i < paragraphs.Count; i++) + { + WParagraph paragraph = paragraphs[i] as WParagraph; + if (paragraph.BreakCharacterFormat.FontName == "Times New Roman") { + paragraph.BreakCharacterFormat.FontName = fontName; + } + } + + // Find all text ranges by EntityType in the Word document. + List textRanges = document.FindAllItemsByProperty(EntityType.TextRange, null, null); + + // Change the font name for all text content in the document. + for (int i = 0; i < textRanges.Count; i++) + { + WTextRange textRange = textRanges[i] as WTextRange; + if (textRange.CharacterFormat.FontName == "Times New Roman") { + textRange.CharacterFormat.FontName = fontName; + } + } + } + private WordDocument GetDocument(ConversionParameters param) { //Hooks MetafileImageParsed event. WordDocument.MetafileImageParsed += OnMetafileImageParsed; - WordDocument document = WordDocument.LoadString(DecodeIfBase64(param.content), GetFormatType(param.from)); + byte[] byteArray = DecodeIfBase64(param.content); + MemoryStream stream = new MemoryStream(byteArray); + + WordDocument document = WordDocument.Load(stream, GetFormatType(param.from)); //Unhooks MetafileImageParsed event. WordDocument.MetafileImageParsed -= OnMetafileImageParsed; + stream.Dispose(); return document; } - internal string ExportPDF(WDocument wordDocument) { // Instantiation of DocIORenderer for Word to PDF conversion. DocIORenderer render = new DocIORenderer(); + //Sets true to embed TrueType fonts + render.Settings.EmbedFonts = true; + render.Settings.EmbedCompleteFonts = true; //Converts Word document into PDF document PdfDocument pdfDocument = render.ConvertToPDF(wordDocument); From 4d5473b472040b9e139d0a6ac9cb85621da4017e Mon Sep 17 00:00:00 2001 From: Victor Rudolfsson Date: Tue, 19 Nov 2024 20:20:07 +1000 Subject: [PATCH 5/5] fix: bump to 27.2.2 --- .../ej2-documenteditor-server.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ej2-documenteditor-server/ej2-documenteditor-server.csproj b/src/ej2-documenteditor-server/ej2-documenteditor-server.csproj index 640e7c8..cae4737 100644 --- a/src/ej2-documenteditor-server/ej2-documenteditor-server.csproj +++ b/src/ej2-documenteditor-server/ej2-documenteditor-server.csproj @@ -49,8 +49,8 @@ - - + +