diff --git a/API.Tests/Controllers/PdfControllerTests.cs b/API.Tests/Controllers/PdfControllerTests.cs index ef9389e..be3dad6 100644 --- a/API.Tests/Controllers/PdfControllerTests.cs +++ b/API.Tests/Controllers/PdfControllerTests.cs @@ -81,7 +81,8 @@ public void ReadFields_ReturnsFieldsResponse_WhenSuccessful() var fileMock = new Mock(); var fields = new List { new TestPdfField("Field1", "Text", 1, false, "FieldValue1") }; var pages = new List { new TestPdfPageInfo(1, 600f, 800f) }; - _pdfServiceMock.Setup(service => service.ReadFields(It.IsAny())).Returns((fields, pages)); + var fonts = new List { "arial-mt", "arial-boldmt", "arial-italicmt", "arial-bolditalicmt" }; + _pdfServiceMock.Setup(service => service.ReadFields(It.IsAny())).Returns((fields, pages, fonts)); // Act var result = _controller.ReadFields(fileMock.Object); @@ -96,6 +97,10 @@ public void ReadFields_ReturnsFieldsResponse_WhenSuccessful() Assert.NotNull(response.Pages); Assert.Single(response.Pages); Assert.Equal(1, response.Pages[0].Number); + + Assert.NotNull(response.Fonts); + Assert.Equal(4, response.Fonts.Count); + Assert.Equal("arial-mt", response.Fonts[0]); } [Fact(DisplayName = "ReadFields returns BadRequest when any exception thrown")] diff --git a/API.Tests/Converters/EnumJsonConverterTests.cs b/API.Tests/Converters/EnumJsonConverterTests.cs new file mode 100644 index 0000000..a77c89d --- /dev/null +++ b/API.Tests/Converters/EnumJsonConverterTests.cs @@ -0,0 +1,45 @@ +using API.Converters; +using System.Text.Json; +using static API.Models.Requests.FillRequest; + +namespace API.Tests.Converters +{ + public class EnumJsonConverterTests + { + private readonly JsonSerializerOptions _options; + + public EnumJsonConverterTests() + { + _options = new JsonSerializerOptions + { + Converters = { new EnumJsonConverter() } + }; + } + + [Theory(DisplayName = "Read single value returns expected value")] + [InlineData("\"left\"", TextHorizontalAlignment.LEFT)] + [InlineData("\"CENTER\"", TextHorizontalAlignment.CENTER)] + public void Read_SingleValue_ReturnsExpectedValue(string json, TextHorizontalAlignment expected) + { + var result = JsonSerializer.Deserialize(json, _options); + Assert.Equal(expected, result); + } + + [Fact(DisplayName = "Read invalid value throws JsonException")] + public void Read_InvalidArray_ThrowsJsonException() + { + var json = "\"somevalue\""; + + Assert.Throws(() => JsonSerializer.Deserialize(json, _options)); + } + + [Theory(DisplayName = "Write Enum value serializes correctly")] + [InlineData(TextHorizontalAlignment.LEFT, "\"left\"")] + [InlineData(TextHorizontalAlignment.RIGHT, "\"right\"")] + public void Write_Value_SerializesCorrectly(TextHorizontalAlignment value, string result) + { + var json = JsonSerializer.Serialize(value, _options); + Assert.Equal(result, json); + } + } +} diff --git a/API.Tests/Converters/FieldValueJsonConverterTests.cs b/API.Tests/Converters/FieldValueJsonConverterTests.cs index 2997e02..d4cba94 100644 --- a/API.Tests/Converters/FieldValueJsonConverterTests.cs +++ b/API.Tests/Converters/FieldValueJsonConverterTests.cs @@ -1,9 +1,5 @@ - -using System; -using System.Collections.Generic; -using System.Text.Json; using API.Converters; -using Xunit; +using System.Text.Json; namespace API.Tests.Converters { @@ -73,14 +69,14 @@ public void Read_InvalidArray_ThrowsJsonException() } [Theory(DisplayName = "Write single value serializes correctly")] - [InlineData("stringValue", "stringValue")] - [InlineData(123, 123)] - [InlineData(true, true)] - [InlineData(false, false)] + [InlineData("stringValue", "\"stringValue\"")] + [InlineData(123, "123")] + [InlineData(true, "true")] + [InlineData(false, "false")] public void Write_Value_SerializesCorrectly(object value, object result) { var json = JsonSerializer.Serialize(value, _options); - Assert.Equal(value, result); + Assert.Equal(result, json); } [Fact(DisplayName = "Write string array serializes correctly")] diff --git a/API.Tests/Services/ITextPdfService/Functional/PdfServiceTests.cs b/API.Tests/Services/ITextPdfService/Functional/PdfServiceTests.cs index 56d30bc..c18144d 100644 --- a/API.Tests/Services/ITextPdfService/Functional/PdfServiceTests.cs +++ b/API.Tests/Services/ITextPdfService/Functional/PdfServiceTests.cs @@ -3,6 +3,8 @@ using iText.Forms; using iText.Forms.Fields; using iText.Kernel.Pdf; +using iText.Kernel.Pdf.Canvas.Parser; +using iText.Kernel.Pdf.Canvas.Parser.Listener; using iText.Kernel.Pdf.Xobject; using Microsoft.AspNetCore.Http; using Moq; @@ -17,7 +19,7 @@ public class PdfServiceTest public PdfServiceTest() { - _pdfService = new PdfService(); + _pdfService = new PdfService(null); } [Fact(DisplayName = "ReadFields should return fields from read PDF")] @@ -65,6 +67,22 @@ public void ReadFields_ShouldReturnPagesInfo() Assert.True(pageInfo.Height > 0); } + [Fact(DisplayName = "ReadFields should return a list of all available fonts from read PDF")] + public void ReadFields_ShouldReturnAvailableFontsList() + { + // Arrange + var pdfFile = CreateSimplePdfForm(new List()); + + // Act + var fonts = _pdfService.ReadFields(pdfFile.Object).fonts; + + // Assert + Assert.NotNull(fonts); + Assert.NotEmpty(fonts); + Assert.Contains("courier", fonts);//One of the fonts automatically registered in the itext library + Assert.Contains("courier-bold", fonts);//One of the fonts automatically registered in the itext library + } + [Fact(DisplayName = "Fill should fill fields in real PDF")] public void Fill_ShouldFillFields() { @@ -158,7 +176,7 @@ public void Fill_ShouldAddImage() Assert.Equal("png", img.IdentifyImageFileExtension()); } - [Fact(DisplayName = "Fill should return an error if the scale is less than or equal to 0 or the width or height of the image after applying the scale or width and height, if specified, is greater than the width or height of the page.")] + [Fact(DisplayName = "Fill should return an error if the scale is less than or equal to 0, or if the image width or height after applying the scale, or the width and height if specified, is greater than the page width or height.")] public void Fill_ShouldThrowError_WhenIncorrectImageSize() { // Arrange @@ -202,6 +220,132 @@ public void Fill_ShouldThrowError_WhenIncorrectImageSize() //Black rectangle 50x30 (.png) private readonly string ImageBase64String = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAeCAIAAADhM9qrAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAcSURBVFhH7cExAQAAAMKg9U9tDQ8gAAAAAK7UABGyAAFALqTGAAAAAElFTkSuQmCC"; + [Fact(DisplayName = "Fill should add a text in a real PDF")] + public void Fill_ShouldAddText() + { + // Arrange + var pdfFile = CreateSimplePdfForm(new List()); + var firstfont = _pdfService.ReadFields(pdfFile.Object).fonts[0]; + + pdfFile.Object.OpenReadStream().Position = 0; + var textToAdd = "Test text1"; + var fieldsToFill = new List{ + new FillRequest.Field + { + Page = 1, + Value = textToAdd, + TextStyle = new FillRequest.TextStyle + { + HorizontalAlignment = FillRequest.TextHorizontalAlignment.CENTER, + VerticalAlignment = FillRequest.TextVerticalAlignment.MIDDLE, + Color = "#00ff00", + Font = new FillRequest.FontStyle { Name = firstfont, Size = 8 } + } + } + }; + + // Act & Assert + var filledPdfBytes = _pdfService.Fill(pdfFile.Object, fieldsToFill); + Assert.NotNull(filledPdfBytes); + + using var pdfDocument = new PdfDocument(new PdfReader(new MemoryStream(filledPdfBytes))); + + var docText = PdfTextExtractor.GetTextFromPage(pdfDocument.GetPage(1), new SimpleTextExtractionStrategy()); + Assert.Equal(textToAdd, docText); + } + + [Fact(DisplayName = "Fill should return an error if the x, y coordinates, or the width or height of the text box, if specified, are greater than the width or height of the page.")] + public void Fill_ShouldThrowError_WhenIncorrectTextFieldPositionOrSize() + { + // Arrange + var pdfFile = CreateSimplePdfForm(new List()); + + var firstPage = _pdfService.ReadFields(pdfFile.Object).pages[0]; + var pageWidth = firstPage.Width; + var pageHeight = firstPage.Height; + + // Arrange + pdfFile.Object.OpenReadStream().Position = 0; + var fieldsToFill = new List{ + new FillRequest.Field { Value = "Test text1", X = pageWidth + 50 , Y = pageHeight + 100 } + }; + + // Act & Assert + var exception = Assert.Throws(() => _pdfService.Fill(pdfFile.Object, fieldsToFill)); + Assert.Contains("Invalid text coordinate X value", exception.Message); + + // Arrange + pdfFile.Object.OpenReadStream().Position = 0; + fieldsToFill = new List{ + new FillRequest.Field { Value = "Test text1", Y = pageHeight + 100 } + }; + + // Act & Assert + exception = Assert.Throws(() => _pdfService.Fill(pdfFile.Object, fieldsToFill)); + Assert.Contains("Invalid text coordinate Y value", exception.Message); + + // Arrange + pdfFile.Object.OpenReadStream().Position = 0; + fieldsToFill = new List{ + new FillRequest.Field { Value = "Test text1", Width = pageWidth + 50 , Height = pageHeight + 100 } + }; + + // Act & Assert + exception = Assert.Throws(() => _pdfService.Fill(pdfFile.Object, fieldsToFill)); + Assert.Contains("Invalid text width value", exception.Message); + + // Arrange + pdfFile.Object.OpenReadStream().Position = 0; + fieldsToFill = new List{ + new FillRequest.Field { Value = "Test text1", Height = pageHeight + 100 } + }; + + // Act & Assert + exception = Assert.Throws(() => _pdfService.Fill(pdfFile.Object, fieldsToFill)); + Assert.Contains("Invalid text height value", exception.Message); + } + + [Fact(DisplayName = "Fill should return an error if the font name is unknown.")] + public void Fill_ShouldThrowError_WhenUnknownFontName() + { + // Arrange + var pdfFile = CreateSimplePdfForm(new List()); + + var fieldsToFill = new List{ + new FillRequest.Field + { + Value = "Test text1", + TextStyle = new FillRequest.TextStyle + { + Font = new FillRequest.FontStyle { Name = "UnknownFont" } + } + } + }; + + // Act & Assert + var exception = Assert.Throws(() => _pdfService.Fill(pdfFile.Object, fieldsToFill)); + Assert.Contains("The font with the name 'UnknownFont' not found", exception.Message); + } + + [Fact(DisplayName = "Fill should return an error if the text color value is invalid.")] + public void Fill_ShouldThrowError_WhenInvalidColorValue() + { + // Arrange + var pdfFile = CreateSimplePdfForm(new List()); + + var fieldsToFill = new List{ + new FillRequest.Field + { + Value = "Test text1", + TextStyle = new FillRequest.TextStyle { Color = "Unknown color" } + } + }; + + // Act & Assert + var exception = Assert.Throws(() => _pdfService.Fill(pdfFile.Object, fieldsToFill)); + Assert.Contains("Unknown color value", exception.Message); + } + private Mock CreateSimplePdfForm(List fields) { var stream = new MemoryStream(); diff --git a/API/Controllers/PdfController.cs b/API/Controllers/PdfController.cs index b37d35d..6588913 100644 --- a/API/Controllers/PdfController.cs +++ b/API/Controllers/PdfController.cs @@ -55,7 +55,7 @@ public IActionResult ReadFields(IFormFile file) { try { - var (fields, pages) = _pdfService.ReadFields(file); + var (fields, pages, fonts) = _pdfService.ReadFields(file); return Ok(new FieldsResponse() { @@ -66,6 +66,7 @@ public IActionResult ReadFields(IFormFile file) Width = p.Width, Height = p.Height }).ToList(), + Fonts = fonts, Fields = fields.Select(f => GetResponseField(f)).ToList() }); } diff --git a/API/Converters/EnumJsonConverter.cs b/API/Converters/EnumJsonConverter.cs new file mode 100644 index 0000000..3c19a78 --- /dev/null +++ b/API/Converters/EnumJsonConverter.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace API.Converters; + +public class EnumJsonConverter : JsonConverter + where TEnum : Enum +{ + public override TEnum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + var stringValue = reader.GetString(); + + if (!Enum.TryParse(typeof(TEnum), stringValue, true, out object? parsedEnum)) + throw new JsonException($"Unexpected enum value: '{stringValue}'. Expected one of the values:{string.Join(", ", Enum.GetNames(typeof(TEnum)))}."); + + return (TEnum)parsedEnum; + } + + throw new JsonException($"Unexpected token type: '{reader.TokenType}'. Expected string type."); + } + + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString().ToLower()); + } +} diff --git a/API/Converters/FieldValueJsonConverter.cs b/API/Converters/FieldValueJsonConverter.cs index 2ff989f..da3c5ef 100644 --- a/API/Converters/FieldValueJsonConverter.cs +++ b/API/Converters/FieldValueJsonConverter.cs @@ -67,6 +67,15 @@ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOp case List intList: JsonSerializer.Serialize(writer, intList, options); break; + case int intValue: + writer.WriteNumberValue(intValue); + break; + case double doubleValue: + writer.WriteNumberValue(doubleValue); + break; + case bool boolValue: + writer.WriteBooleanValue(boolValue); + break; default: writer.WriteStringValue(value.ToString()); break; diff --git a/API/Models/Requests/FillRequest.cs b/API/Models/Requests/FillRequest.cs index 0dffc9f..3cdb83d 100644 --- a/API/Models/Requests/FillRequest.cs +++ b/API/Models/Requests/FillRequest.cs @@ -8,9 +8,9 @@ namespace API.Models.Requests; [ModelBinder(BinderType = typeof(FillRequestBinder))] public class FillRequest -{ +{ public required IFormFile file { get; set; } - + public required FieldsData data { get; set; } public class FieldsData @@ -31,8 +31,46 @@ public class Field public float? Width { get; set; } public float? Height { get; set; } public string? Type { get; set; } + public TextStyle? TextStyle { get; set; } public int? Page { get; set; } } + + public class TextStyle + { + public float? Leading { get; set; } + public string? Color { get; set; } + + [JsonConverter(typeof(EnumJsonConverter))] + public TextHorizontalAlignment? HorizontalAlignment { get; set; } + + [JsonConverter(typeof(EnumJsonConverter))] + public TextVerticalAlignment? VerticalAlignment { get; set; } + + public FontStyle? Font { get; set; } + } + + public enum TextHorizontalAlignment + { + LEFT, + CENTER, + RIGHT + } + + public enum TextVerticalAlignment + { + TOP, + MIDDLE, + BOTTOM + } + + public class FontStyle + { + public string? Name { get; set; } = null; + public float? Size { get; set; } + public bool? Bold { get; set; } + public bool? Italic { get; set; } + public bool? Underline { get; set; } + } } /** diff --git a/API/Models/Responses/FieldsResponse.cs b/API/Models/Responses/FieldsResponse.cs index 90d079f..fd6dda6 100644 --- a/API/Models/Responses/FieldsResponse.cs +++ b/API/Models/Responses/FieldsResponse.cs @@ -3,6 +3,7 @@ namespace API.Models.Responses; public class FieldsResponse { public List? Pages { get; set; } + public List? Fonts { get; set; } public int FieldsCount { get; set; } public List? Fields { get; set; } diff --git a/API/Program.cs b/API/Program.cs index c1d25c2..30302ba 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Versioning; -using Microsoft.OpenApi.Models; using Microsoft.Extensions.Options; using NLog.Extensions.Logging; using Swashbuckle.AspNetCore.SwaggerGen; diff --git a/API/Resources/Fonts/arial.ttf b/API/Resources/Fonts/arial.ttf new file mode 100644 index 0000000..8682d94 Binary files /dev/null and b/API/Resources/Fonts/arial.ttf differ diff --git a/API/Resources/Fonts/arialbd.ttf b/API/Resources/Fonts/arialbd.ttf new file mode 100644 index 0000000..a6037e6 Binary files /dev/null and b/API/Resources/Fonts/arialbd.ttf differ diff --git a/API/Resources/Fonts/arialbi.ttf b/API/Resources/Fonts/arialbi.ttf new file mode 100644 index 0000000..6a1fa0f Binary files /dev/null and b/API/Resources/Fonts/arialbi.ttf differ diff --git a/API/Resources/Fonts/ariali.ttf b/API/Resources/Fonts/ariali.ttf new file mode 100644 index 0000000..3801997 Binary files /dev/null and b/API/Resources/Fonts/ariali.ttf differ diff --git a/API/Resources/Fonts/ariblk.ttf b/API/Resources/Fonts/ariblk.ttf new file mode 100644 index 0000000..e7ae345 Binary files /dev/null and b/API/Resources/Fonts/ariblk.ttf differ diff --git a/API/Resources/Fonts/calibri.ttf b/API/Resources/Fonts/calibri.ttf new file mode 100644 index 0000000..aac4726 Binary files /dev/null and b/API/Resources/Fonts/calibri.ttf differ diff --git a/API/Resources/Fonts/calibrib.ttf b/API/Resources/Fonts/calibrib.ttf new file mode 100644 index 0000000..326893d Binary files /dev/null and b/API/Resources/Fonts/calibrib.ttf differ diff --git a/API/Resources/Fonts/calibrii.ttf b/API/Resources/Fonts/calibrii.ttf new file mode 100644 index 0000000..3543c4c Binary files /dev/null and b/API/Resources/Fonts/calibrii.ttf differ diff --git a/API/Resources/Fonts/calibril.ttf b/API/Resources/Fonts/calibril.ttf new file mode 100644 index 0000000..1ac6669 Binary files /dev/null and b/API/Resources/Fonts/calibril.ttf differ diff --git a/API/Resources/Fonts/calibrili.ttf b/API/Resources/Fonts/calibrili.ttf new file mode 100644 index 0000000..fac9ae2 Binary files /dev/null and b/API/Resources/Fonts/calibrili.ttf differ diff --git a/API/Resources/Fonts/calibriz.ttf b/API/Resources/Fonts/calibriz.ttf new file mode 100644 index 0000000..58afcd1 Binary files /dev/null and b/API/Resources/Fonts/calibriz.ttf differ diff --git a/API/Resources/Fonts/comic.ttf b/API/Resources/Fonts/comic.ttf new file mode 100644 index 0000000..2d8e9ca Binary files /dev/null and b/API/Resources/Fonts/comic.ttf differ diff --git a/API/Resources/Fonts/comicbd.ttf b/API/Resources/Fonts/comicbd.ttf new file mode 100644 index 0000000..59f77d5 Binary files /dev/null and b/API/Resources/Fonts/comicbd.ttf differ diff --git a/API/Resources/Fonts/comici.ttf b/API/Resources/Fonts/comici.ttf new file mode 100644 index 0000000..49a4069 Binary files /dev/null and b/API/Resources/Fonts/comici.ttf differ diff --git a/API/Resources/Fonts/comicz.ttf b/API/Resources/Fonts/comicz.ttf new file mode 100644 index 0000000..f451961 Binary files /dev/null and b/API/Resources/Fonts/comicz.ttf differ diff --git a/API/Services/IPdfService.cs b/API/Services/IPdfService.cs index 3bc59f7..41fe750 100644 --- a/API/Services/IPdfService.cs +++ b/API/Services/IPdfService.cs @@ -5,6 +5,6 @@ namespace API.Services; public interface IPdfService { - (List fields, List pages) ReadFields(IFormFile pdfFile); + (List fields, List pages, List fonts) ReadFields(IFormFile pdfFile); byte[] Fill(IFormFile pdfFile, List fields); } diff --git a/API/Services/ITextPdfService/PdfService.cs b/API/Services/ITextPdfService/PdfService.cs index 2d13d84..7b33571 100644 --- a/API/Services/ITextPdfService/PdfService.cs +++ b/API/Services/ITextPdfService/PdfService.cs @@ -1,32 +1,54 @@ using API.Models.Pdf; using API.Models.Requests; using API.Services.ITextPdfService.Fields; +using iText.Commons.Utils; using iText.Forms; using iText.Forms.Fields; +using iText.IO.Font; using iText.IO.Image; using iText.IO.Source; -using iText.Kernel.Geom; +using iText.Kernel.Colors; +using iText.Kernel.Font; using iText.Kernel.Pdf; using iText.Kernel.Pdf.Action; using iText.Layout; using iText.Layout.Element; +using NLog; +using System.Drawing; using System.Text.RegularExpressions; +using static API.Models.Requests.FillRequest; +using IO = System.IO; +using iTextProperties = iText.Layout.Properties; namespace API.Services.ITextPdfService; public class PdfService : IPdfService { - public (List fields, List pages) ReadFields(IFormFile pdfFile) + private IConfiguration? _configuration; + private Logger _logger; + + public PdfService(IConfiguration? configuration) + { + _configuration = configuration; + _logger = NLog.LogManager.GetCurrentClassLogger(); + } + + public (List fields, List pages, List fonts) ReadFields(IFormFile pdfFile) { PdfDocument pdfDocument = OpenPdfDocument(pdfFile, PdfFileOpenMode.Read, out _); - return (ReadAllFormFields(pdfDocument, false), ReadAllPagesInfo(pdfDocument)); + var fontsHelper = new FontsHelper(pdfDocument, _configuration, _logger); + + return (ReadAllFormFields(pdfDocument, false), ReadAllPagesInfo(pdfDocument), fontsHelper.ReadAllAvailableFontsKeys()); } public byte[] Fill(IFormFile pdfFile, List fields) { PdfDocument pdfDocument = OpenPdfDocument(pdfFile, PdfFileOpenMode.ReadWrite, out var outputStream); Document document = new Document(pdfDocument); - + + var fontsHelper = new FontsHelper(pdfDocument, _configuration, _logger); + var colorConverter = new ColorConverter(); + List formFields = ReadAllFormFields(pdfDocument, true); // field.Name is not null here because of the ReadAllFormFields method @@ -47,22 +69,24 @@ public byte[] Fill(IFormFile pdfFile, List fields) { if (field.Value != null && field.Value is string) { + //Page data var pageNum = field.Page ?? 1; var pagesCount = pdfDocument.GetNumberOfPages(); if (pageNum < 1 || pageNum > pagesCount) throw new ArgumentException($"Incorrect page number: '{pageNum}'. The expected value is in the range [1, {pagesCount}]."); + + PdfPage page = pdfDocument.GetPage(pageNum); + var pageSize = page.GetPageSize(); + var pageWidth = pageSize.GetWidth(); + var pageHeight = pageSize.GetHeight(); + // var stringValue = field.Value as string; if (IsBase64String(stringValue!, out Span buffer)) //Image { - PdfPage page = pdfDocument.GetPage(pageNum); - Rectangle pageSize = page.GetPageSize(); - var pageWidth = pageSize.GetWidth(); - var pageHeight = pageSize.GetHeight(); - ImageData data = ImageDataFactory.Create(buffer.ToArray()); - Image image = new Image(data); + var image = new Image(data); var imageRealWidth = image.GetImageWidth(); var imageRealHeight = image.GetImageHeight(); @@ -101,13 +125,107 @@ public byte[] Fill(IFormFile pdfFile, List fields) var imageX = field.X ?? 0; var y = field.Y ?? 0; var imageY = pageHeight - y - imageHeight; + image.SetFixedPosition(pageNum, imageX, imageY);//The center of the coordinate system is the bottom-left corner document.Add(image); } else //Text box { - //to do + var text = new Text(stringValue); + var paragraph = new Paragraph(text); + + //position + var textX = field.X ?? 0; + var y = field.Y ?? 0; + + if (textX < 0 || textX > pageWidth) + throw new ArgumentException($"Invalid text coordinate X value: '{textX}'. The value must be greater than 0 and less than the width of the page: '{pageWidth}'."); + if (y < 0 || y > pageHeight) + throw new ArgumentException($"Invalid text coordinate Y value: '{y}'. The value must be greater than 0 and less than the height of the page: '{pageHeight}'."); + + if (field.Width != null) + { + var textRectWidth = field.Width.Value; + if (textRectWidth <= 0 || textRectWidth > pageWidth - textX) + throw new ArgumentException($"Invalid text width value: '{textRectWidth}'. The width must be greater than 0 and less than: '{pageWidth - textX}'."); + paragraph.SetWidth(textRectWidth); + } + else + paragraph.SetWidth(pageWidth - textX); + + if (field.Height != null) + { + var textRectHeight = field.Height.Value; + if (textRectHeight <= 0 || textRectHeight > pageHeight) + throw new ArgumentException($"Invalid text height value: '{textRectHeight}'. The height must be greater than 0 and less than the height of the page: '{pageHeight}'."); + paragraph.SetHeight(textRectHeight); + } + else + paragraph.SetHeight(pageHeight - y); + + var textY = pageHeight - y - paragraph.GetHeight().GetValue(); + paragraph.SetFixedPosition(pageNum, textX, textY, paragraph.GetWidth().GetValue()); + + // + var textStyle = field.TextStyle; + if (textStyle != null) + { + paragraph.SetMultipliedLeading(textStyle.Leading.HasValue ? textStyle.Leading.Value : 1); + + FontStyle? fontStyle = textStyle.Font; + if (fontStyle != null) + { + if (fontStyle.Size.HasValue) + paragraph.SetFontSize(fontStyle.Size.Value); + + if (fontStyle.Name != null) + { + if (!fontsHelper.FontExists(fontStyle.Name)) + throw new ArgumentException($"The font with the name '{fontStyle.Name}' not found."); + + PdfFont? font = fontsHelper.CreateFont(fontStyle.Name); + if (font != null) + paragraph.SetFont(font); + } + + if ((fontStyle.Bold ?? false) && !fontsHelper.IsFontBold(fontStyle.Name)) + text.SimulateBold(); + + if ((fontStyle.Italic ?? false) && !fontsHelper.IsFontItalic(fontStyle.Name)) + text.SimulateItalic(); + + if (fontStyle.Underline ?? false) + text.SetUnderline(); + } + + if (textStyle.HorizontalAlignment.HasValue) + { + if (Enum.TryParse(textStyle.HorizontalAlignment.Value.ToString(), true, out iTextProperties.TextAlignment horizontalAlignment)) + paragraph.SetTextAlignment(horizontalAlignment); + else + _logger.Warn($"Unknown text HorizontalAlignment value '{textStyle.HorizontalAlignment.Value}'."); + } + + if (textStyle.VerticalAlignment.HasValue) + { + if (Enum.TryParse(textStyle.VerticalAlignment.Value.ToString(), true, out iTextProperties.VerticalAlignment verticalAlignment)) + paragraph.SetVerticalAlignment(verticalAlignment); + else + _logger.Warn($"Unknown text VerticalAlignment value '{textStyle.VerticalAlignment.Value}'."); + } + + if (textStyle.Color != null) + { + if (!colorConverter.IsValid(textStyle.Color)) + throw new ArgumentException($"Unknown color value '{textStyle.Color}'."); + + var color = (System.Drawing.Color)colorConverter.ConvertFromInvariantString(textStyle.Color)!; + paragraph.SetFontColor(new DeviceRgb(color.R, color.G, color.B), color.A / 255f); + } + } + + document.Add(paragraph); } } } @@ -123,7 +241,7 @@ public byte[] Fill(IFormFile pdfFile, List fields) throw new InvalidOperationException("The output stream is null"); } - public bool IsBase64String(string input, out Span buffer) + private bool IsBase64String(string input, out Span buffer) { buffer = null; @@ -187,6 +305,186 @@ private List ReadAllPagesInfo(PdfDocument pdfDocument) } } +class FontsHelper +{ + PdfDocument _pdfDocument; + IConfiguration? _configuration; + Logger _logger; + + public FontsHelper(PdfDocument pdfDocument, IConfiguration? configuration, Logger logger) + { + _pdfDocument = pdfDocument; + _configuration = configuration; + _logger = logger; + } + + public bool FontExists(string fontName) + { + return AllAvailableFonts.Contains(fontName.Trim().ToLowerInvariant()); + } + + public bool IsFontBold(string? fontName) + { + return fontName != null && fontName.ToLowerInvariant().Contains("bold"); + } + + public bool IsFontItalic(string? fontName) + { + fontName = fontName?.ToLowerInvariant(); + return fontName != null && (fontName.Contains("italic") || fontName.Contains("oblique")); + } + + public PdfFont? CreateFont(string fontName) + { + fontName = fontName.Trim().ToLowerInvariant(); + + if (FontExists(fontName)) + { + if (FontProgramFactory.GetRegisteredFonts().Contains(fontName)) + { + return PdfFontFactory.CreateRegisteredFont(fontName); + } + + var allFontsFromResources = GetAllFontsFromResources(); + if (allFontsFromResources.ContainsKey(fontName)) + { + return PdfFontFactory.CreateFont(allFontsFromResources[fontName]); + } + + var allDocumentFonts = GetAllDocumentFonts(); + if (allDocumentFonts.ContainsKey(fontName)) + { + return PdfFontFactory.CreateFont(allDocumentFonts[fontName]); + } + } + + return null; + } + + List? _allAvailableFonts; + List AllAvailableFonts + { + get + { + return ReadAllAvailableFontsKeys(); + } + } + + public List ReadAllAvailableFontsKeys() + { + if (_allAvailableFonts == null) + { + var allFonts = new List(); + + allFonts.AddRange(FontProgramFactory.GetRegisteredFonts().Select(f => f.ToLowerInvariant())); + allFonts.AddRange(GetAllDocumentFonts().Keys.Select(f => f.ToLowerInvariant())); + allFonts.AddRange(GetAllFontsFromResources().Keys); + + _allAvailableFonts = allFonts.Distinct().ToList(); + } + + return _allAvailableFonts; + } + + Dictionary? _documentFonts; + private Dictionary GetAllDocumentFonts() + { + if (_documentFonts == null) + { + _documentFonts = new Dictionary(); + + var pagesCount = _pdfDocument.GetNumberOfPages(); + for (int i = 1; i <= pagesCount; i++) + { + var page = _pdfDocument.GetPage(i); + PdfDictionary fontsDict = page.GetResources().GetResource(PdfName.Font); + + if (fontsDict != null) + { + foreach (PdfName key in fontsDict.KeySet()) + { + var fontDict = fontsDict.GetAsDictionary(key); + if (fontDict != null) + { + string? fontName = fontDict.GetAsName(PdfName.BaseFont)?.GetValue().ToLowerInvariant(); + + if (!string.IsNullOrEmpty(fontName) && !_documentFonts.ContainsKey(fontName)) + _documentFonts.Add(fontName, fontDict); + } + } + } + } + } + + return _documentFonts; + } + + Dictionary? _resourcesFonts; + private Dictionary GetAllFontsFromResources() + { + if (_resourcesFonts == null) + { + _resourcesFonts = new Dictionary(); + + if (_configuration != null) + { + var fontsPaths = _configuration.GetSection("Resources:Fonts"); + if (fontsPaths != null ) + { + foreach (var pathSection in fontsPaths.GetChildren()) + { + var path = pathSection.Value; + if (!string.IsNullOrEmpty(path)) + { + DirectoryInfo directoryInfo = new DirectoryInfo(path); + if (directoryInfo.Exists) + { + List files = directoryInfo.GetFiles().ToList(); + + foreach (var file in files) + { + String fileExtension = file.Extension.ToLowerInvariant(); + switch (fileExtension) + { + case ".afm": + case ".pfm": + String pfb = IO.Path.ChangeExtension(file.FullName, ".pfb"); + if (FileUtil.FileExists(pfb)) + AddToFontList(file.FullName); + break; + + case ".ttf": + case ".otf": + AddToFontList(file.FullName); + break; + } + } + } + else + { + _logger.Warn($"Fonts directory '{path}' not found."); + } + } + } + } + + void AddToFontList(string fontPath) + { + FontProgramDescriptor descriptor = FontProgramDescriptorFactory.FetchDescriptor(fontPath); + if (descriptor != null) + { + var fontName = descriptor.GetFontNameLowerCase(); + if (!_resourcesFonts.ContainsKey(fontName)) + _resourcesFonts.Add(fontName, fontPath); + } + } + } + } + + return _resourcesFonts; + } +} + public enum PdfFileOpenMode { Read, diff --git a/API/appsettings.json b/API/appsettings.json index 75528e0..7311029 100644 --- a/API/appsettings.json +++ b/API/appsettings.json @@ -1,10 +1,13 @@ -{ +{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } - }, + }, "AllowedHosts": "*", - "AllowedOrigins": "http://localhost" + "AllowedOrigins": "http://localhost", + "Resources": { + "Fonts": [ "Resources/Fonts" ] + } }