commit cecde2bc1cb656f65922e69e0d132b68fd1c976e Author: huanld Date: Mon May 18 15:05:10 2026 +0700 feat: standalone ExcelToPdfService - ASP.NET Core 8 + Syncfusion - Services/ExcelToPdfService.cs: core conversion logic (XlsIORenderer) - Controllers/ExcelToPdfController.cs: REST API endpoints * POST /api/ExcelToPdf/convert (file upload) * POST /api/ExcelToPdf/convert-by-path (server path) * GET /api/ExcelToPdf/health - Program.cs: minimal API setup, 50MB upload limit - appsettings.json: port 5200, Syncfusion license - Syncfusion 21.1.37 (XlsIO + Pdf + XlsIORenderer) - Binary signed: Azure Trusted Signing (Private Trust) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fc8afb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +publish/ +*.user +.vs/ diff --git a/Controllers/ExcelToPdfController.cs b/Controllers/ExcelToPdfController.cs new file mode 100644 index 0000000..d746d0c --- /dev/null +++ b/Controllers/ExcelToPdfController.cs @@ -0,0 +1,63 @@ +using ExcelToPdfService.Services; +using Microsoft.AspNetCore.Mvc; + +namespace ExcelToPdfService.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ExcelToPdfController : ControllerBase +{ + private readonly IExcelToPdfService _service; + + public ExcelToPdfController(IExcelToPdfService service) => _service = service; + + /// Upload Excel file → nhận PDF trả về + [HttpPost("convert")] + public async Task Convert( + IFormFile file, + [FromQuery] string? printArea = null, + [FromQuery] bool landscape = false) + { + if (file == null || file.Length == 0) + return BadRequest(new { success = false, message = "No file uploaded" }); + + var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); + if (ext != ".xlsx" && ext != ".xls") + return BadRequest(new { success = false, message = "Only .xlsx/.xls files are supported" }); + + await using var stream = file.OpenReadStream(); + var pdfBytes = await _service.ConvertAsync(stream, printArea, landscape); + var fileName = Path.GetFileNameWithoutExtension(file.FileName) + ".pdf"; + + return File(pdfBytes, "application/pdf", fileName); + } + + /// Chuyển đổi file Excel theo đường dẫn trên server + [HttpPost("convert-by-path")] + public async Task ConvertByPath([FromBody] ConvertByPathRequest req) + { + if (string.IsNullOrWhiteSpace(req.FilePath)) + return BadRequest(new { success = false, message = "FilePath is required" }); + + try + { + var pdfBytes = await _service.ConvertFileAsync(req.FilePath, req.PrintArea, req.Landscape); + var fileName = Path.GetFileNameWithoutExtension(req.FilePath) + ".pdf"; + return File(pdfBytes, "application/pdf", fileName); + } + catch (FileNotFoundException) + { + return NotFound(new { success = false, message = $"File not found: {req.FilePath}" }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = ex.Message }); + } + } + + /// Health check + [HttpGet("health")] + public IActionResult Health() => Ok(new { status = "ok", version = "1.0.0", time = DateTime.UtcNow }); +} + +public record ConvertByPathRequest(string FilePath, string? PrintArea = null, bool Landscape = false); diff --git a/ExcelToPdfService.csproj b/ExcelToPdfService.csproj new file mode 100644 index 0000000..17c54dc --- /dev/null +++ b/ExcelToPdfService.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/ExcelToPdfService.http b/ExcelToPdfService.http new file mode 100644 index 0000000..450c5cd --- /dev/null +++ b/ExcelToPdfService.http @@ -0,0 +1,6 @@ +@ExcelToPdfService_HostAddress = http://localhost:5285 + +GET {{ExcelToPdfService_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..5fa65b6 --- /dev/null +++ b/Program.cs @@ -0,0 +1,30 @@ +using ExcelToPdfService.Services; +using Syncfusion.Licensing; + +var builder = WebApplication.CreateBuilder(args); + +// Syncfusion license +SyncfusionLicenseProvider.RegisterLicense( + builder.Configuration["Syncfusion:LicenseKey"] + ?? "MjExMDkzMEAzMjMxMmUzMTJlMzMzNWd1RGM1NVFmUUMzWmZlN0dCc3NadUJyM1RUYVh1SHVSS1B2Tzdwa0NhcE09"); + +// Services +builder.Services.AddControllers(); +builder.Services.AddScoped(); + +// Allow large file uploads (50 MB) +builder.Services.Configure(o => +{ + o.MultipartBodyLengthLimit = 50 * 1024 * 1024; +}); +builder.WebHost.ConfigureKestrel(o => +{ + o.Limits.MaxRequestBodySize = 50 * 1024 * 1024; +}); + +var app = builder.Build(); + +app.UseHttpsRedirection(); +app.MapControllers(); + +app.Run(); diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..dd040a1 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:12076", + "sslPort": 44368 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "http://localhost:5285", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "https://localhost:7139;http://localhost:5285", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Services/ExcelToPdfService.cs b/Services/ExcelToPdfService.cs new file mode 100644 index 0000000..1a2c1c0 --- /dev/null +++ b/Services/ExcelToPdfService.cs @@ -0,0 +1,65 @@ +using Syncfusion.XlsIO; +using Syncfusion.XlsIORenderer; + +namespace ExcelToPdfService.Services; + +public interface IExcelToPdfService +{ + Task ConvertAsync(Stream excelStream, string? printArea = null, bool landscape = false); + Task ConvertFileAsync(string filePath, string? printArea = null, bool landscape = false); +} + +public class ExcelToPdfService : IExcelToPdfService +{ + public Task ConvertAsync(Stream excelStream, string? printArea = null, bool landscape = false) + { + using var excelEngine = new ExcelEngine(); + var app = excelEngine.Excel; + app.DefaultVersion = ExcelVersion.Xlsx; + + var workbook = app.Workbooks.Open(excelStream); + return Task.FromResult(Convert(workbook, printArea, landscape)); + } + + public Task ConvertFileAsync(string filePath, string? printArea = null, bool landscape = false) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException("Excel file not found", filePath); + + using var excelEngine = new ExcelEngine(); + var app = excelEngine.Excel; + app.DefaultVersion = ExcelVersion.Xlsx; + + using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); + var workbook = app.Workbooks.Open(fs); + return Task.FromResult(Convert(workbook, printArea, landscape)); + } + + private static byte[] Convert(IWorkbook workbook, string? printArea, bool landscape) + { + var sheet = workbook.Worksheets[0]; + + // Page setup + sheet.PageSetup.FitToPagesWide = 1; + sheet.PageSetup.FitToPagesTall = 0; + sheet.PageSetup.Orientation = landscape + ? ExcelPageOrientation.Landscape + : ExcelPageOrientation.Portrait; + + if (!string.IsNullOrWhiteSpace(printArea)) + sheet.PageSetup.PrintArea = printArea; + + // Convert + var renderer = new XlsIORenderer(); + var settings = new XlsIORendererSettings + { + LayoutOptions = LayoutOptions.Automatic + }; + + var pdf = renderer.ConvertToPDF(workbook, settings); + + using var ms = new MemoryStream(); + pdf.Save(ms); + return ms.ToArray(); + } +} diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..d80a40a --- /dev/null +++ b/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Syncfusion": { + "LicenseKey": "MjExMDkzMEAzMjMxMmUzMTJlMzMzNWd1RGM1NVFmUUMzWmZlN0dCc3NadUJyM1RUYVh1SHVSS1B2Tzdwa0NhcE09" + }, + "Urls": "http://0.0.0.0:5200" +}