翻译|使用教程|编辑:李显亮|2020-11-03 09:59:45.900|阅读 341 次
概述:在各种业务环境中,将各种文档合并为一个PDF是客户最常问的问题之一。本文演示了如何使用ASP.NET Core框架将多个文档合并到一个PDF中。
# 慧都年终大促·界面/图表报表/文档/IDE等千款热门软控件火热促销中 >>
在各种业务环境中,将各种文档合并为一个PDF是客户最常问的问题之一。例如,假设您的组织有多个应用程序以XPS和PDF生成特定的文档,使用扫描的图像,并且您的用户希望将其中一些文档合并为一个PDF。
本文演示了如何使用ASP.NET Core框架将多个文档合并到一个PDF中。Aspose.PDF提出了几种使用.NET合并PDF的方法,这些内容在本文中进行了介绍。在本文中,将讨论以下主题:
在本文中,我们将创建一个简单的ASP.NET Web API应用程序,该应用程序允许我们上载文档,选择2个或更多文件进行合并以及下载结果。
(安装包仅提供部分功能,并设置限制,如需试用完整功能请。)
实施ASP.NET Core Web App以将各种文档合并为PDF
步骤1:创建一个ASP.NET Core Web应用程序
我们将为此应用程序使用Web应用程序(模型-视图-控制器)模板。
创建基本应用程序后,我们将需要执行一些其他操作。
"Folders": { "Files": "files", "Temporary" : "temp" }
步骤2:实施Web API控制器以管理服务器上的文件
我们的控制器应执行以下操作:
using Aspose.Demo.Pdf.Merger.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.Extensions.Configuration; namespace Aspose.Demo.Pdf.Merger.Controllers { [Route("api/[controller]")] [ApiController] public class FilesController : ControllerBase { private readonly Dictionary<string, string> _contentType; private readonly ILogger<FilesController> _logger; private readonly string _storageRootFolder; public FilesController(ILogger<FilesController> logger, IWebHostEnvironment env, IConfiguration configuration) { _logger = logger; _storageRootFolder = Path.Combine(env.WebRootPath, configuration["Folders:Files"]); _contentType = new Dictionary<string, string> { { ".txt", "text/plain"}, { ".pdf", "application/pdf"}, { ".doc", "application/vnd.ms-word"}, { ".docx", "application/vnd.ms-word"}, { ".xls", "application/vnd.ms-excel"}, { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, { ".png", "image/png"}, { ".jpg", "image/jpeg"}, { ".jpeg", "image/jpeg"}, { ".gif", "image/gif"}, { ".csv", "text/csv"} }; } // GET: /api/files [HttpGet] public IEnumerable<FileViewModel> GetFiles() { _logger.LogInformation($"Get files from {_storageRootFolder}"); var files = new DirectoryInfo(_storageRootFolder).EnumerateFiles("*.pdf").ToList(); files.AddRange(new DirectoryInfo(_storageRootFolder).EnumerateFiles("*.jpg")); files.AddRange(new DirectoryInfo(_storageRootFolder).EnumerateFiles("*.oxps")); //TODO: add other file types below return files.Select(f => new FileViewModel { Name = f.Name, Size = f.Length }); } [HttpGet("{id}")] public IActionResult OnGetFile(string id) { _logger.LogInformation($"Get file {id}"); var fileName = Path.Combine(_storageRootFolder, id); return File(System.IO.File.OpenRead(fileName), _contentType[Path.GetExtension(fileName)]); } [HttpDelete("{id}")] public IActionResult OnDeleteFile(string id) { _logger.LogInformation($"Delete file {id}"); var fileName = Path.Combine(_storageRootFolder, id); System.IO.File.Delete(fileName); return Ok(); } } }
然后将使用附加的库Resumable.JS来加载文件,因此将与加载文件相关的代码移至单独的控制器是有意义的。
步骤3:实现Web API控制器以使用Resumable.JS上传文件
Resumable.JS库的主要功能是它允许您分块加载文件。因此,我们需要实现一些方法来处理此过程:
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System.IO; using Microsoft.Extensions.Configuration; namespace Aspose.Demo.Pdf.Merger.Controllers { [Route("api/[controller]")] [ApiController] public class UploadController : ControllerBase { private readonly ILogger_logger; private readonly string _storageRootFolder; private readonly string _filesRootFolder; public UploadController( ILoggerlogger, IConfiguration configuration, IWebHostEnvironment env) { _logger = logger; _storageRootFolder = Path.Combine(env.WebRootPath, configuration["Folders:Temporary"]); _filesRootFolder = Path.Combine(env.WebRootPath, configuration["Folders:Files"]); if (!Directory.Exists(_storageRootFolder)) Directory.CreateDirectory(_storageRootFolder); } [HttpOptions] public object UploadFileOptions() { return Ok(); } [HttpGet] public object Upload(int resumableChunkNumber, string resumableIdentifier) { _logger.LogInformation($"Check if chunck {resumableChunkNumber} from {resumableIdentifier} is here."); return ChunkIsHere(resumableChunkNumber, resumableIdentifier) ? Ok() : StatusCode(418); } [HttpPost] public IActionResult Upload( [FromQuery(Name = "ResumableIdentifier")] string resumableIdentifier, [FromQuery(Name = "ResumableFilename")] string resumableFilename, [FromQuery(Name = "ResumableChunkNumber")] int resumableChunkNumber, [FromQuery(Name = "ResumableTotalChunks")] int resumableTotalChunks, IFormFile file) { _logger.LogInformation(file.FileName); var stream = System.IO.File.Create(GetChunkFileName(resumableChunkNumber, resumableIdentifier)); file.CopyTo(stream); stream.Close(); TryAssembleFile(resumableFilename, resumableIdentifier, resumableTotalChunks); return Ok(); } #region Chunk methods [NonAction] private string GetChunkFileName(int chunkNumber, string identifier) { return Path.Combine(_storageRootFolder, $"{identifier}_{chunkNumber}"); } [NonAction] private string GetFilePath(string identifier) { return Path.Combine(_storageRootFolder, identifier); } [NonAction] private bool ChunkIsHere(int chunkNumber, string identifier) { return System.IO.File.Exists(GetChunkFileName(chunkNumber, identifier)); } [NonAction] private bool AllChunksAreHere(string identifier, int chunks) { for (var chunkNumber = 1; chunkNumber <= chunks; chunkNumber++) if (!ChunkIsHere(chunkNumber, identifier)) return false; return true; } [NonAction] private void DeleteChunks(string identifier, int chunks) { for (var chunkNumber = 1; chunkNumber <= chunks; chunkNumber++) { var chunkFileName = GetChunkFileName(chunkNumber, identifier); System.IO.File.Delete(chunkFileName); } } [NonAction] private string ConsolidateFile(string identifier, int chunks) { var path = GetFilePath(identifier); using var destStream = System.IO.File.Create(path, 15000); for (var chunkNumber = 1; chunkNumber <= chunks; chunkNumber++) { var chunkFileName = GetChunkFileName(chunkNumber, identifier); using var sourceStream = System.IO.File.OpenRead(chunkFileName); sourceStream.CopyTo(destStream); } destStream.Close(); return path; } [NonAction] private void TryAssembleFile(string rfn, string ri, int rtc) { if (AllChunksAreHere(ri, rtc)) { // Create a single file var path = ConsolidateFile(ri, rtc); // Move consolidated file System.IO.File.Move(path, Path.Combine(_filesRootFolder, rfn),true); // Delete chunk files DeleteChunks(ri, rtc); } } #endregion } }
该库将标识符用于内部目的。它可以以不同的方式生成。在示例应用程序中,我们使用了一个单独的控制器。
using Microsoft.AspNetCore.Mvc; using System; using System.Linq; namespace Aspose.Demo.Pdf.Merger.Controllers { [Route("api/[controller]")] [ApiController] public class TokenController : ControllerBase { // GET: api/Token?id=<filename> [HttpGet("{id}")] public string OnGet(string id) { var hash = new System.Security.Cryptography.SHA1Managed() .ComputeHash(System.Text.Encoding.UTF8.GetBytes(id + DateTime.Now.Ticks.ToString())); return string.Concat(hash.Select(b => b.ToString("x2"))); } } }
步骤4:为合并的应用程序实现Web UI
现在,我们可以开始实现Web界面了。在示例应用程序中,我们没有使用Angular,React Vue或其他框架,但是我们实现了基于Bootstrap和JQuery的单页应用程序。应用程序页面可以分为两个部分:
由于该网页的代码量很大,因此在此不再显示,我们将完全局限于描述该算法的两个想法。
以下代码段演示了这两种操作的处理程序:
let lastIndex = 0; function selectFileClickHandler() { let order = parseInt($(this).attr('data-order')); if (order > 0) { $(this).attr('data-order', '0'); $(this).find('span').hide('slow'); for (let cell of $("*[data-order]")) { let currentOrder = parseInt(cell.dataset.order); if (currentOrder > order) { cell.dataset.order = currentOrder - 1; cell.firstElementChild.innerHTML = currentOrder - 1; } } lastIndex--; } else { $(this).attr('data-order', ++lastIndex); $(this).find('span').html(lastIndex); $(this).find('span').show('slow'); } $('#btnMerge').prop('disabled', lastIndex<2); } $('#btnMerge').click((e) => { e.preventDefault(); const files = $('*[data-order]').sort(function (a, b) { const contentA = parseInt($(a).data('order')); const contentB = parseInt($(b).data('order')); return (contentA < contentB) ? -1 : (contentA > contentB) ? 1 : 0; }); const data = []; for (let file of files) { const currentOrder = parseInt(file.dataset.order); if (currentOrder > 0) data.push(file.dataset.id); } fetch('api/merge/', { method: 'POST', mode: 'cors', cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, redirect: 'follow', referrerPolicy: 'no-referrer', body: JSON.stringify(data) } ) .then(res => res.json()) .then(res => { console.log(res); refreshFileTable(); }) .catch(err => alert(err)); lastIndex = 0; });
将各种文档合并为PDF
完成准备阶段后,我们可以考虑项目的主要部分。.NET库的Aspose.PDF提供了几种合并文档的方法。您可以在上一篇文章中学习其中的一些内容,但是现在我们将重点介绍一下,并讨论影响PDF中任何文档的可能性。
实际上,如果文档为PDF格式,那么我们必须执行两个操作,然后合并;如果文档不是PDF,则首先进行转换然后合并。
步骤1:实施Web API控制器以将各种文档合并为PDF
using Aspose.Pdf; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Drawing; using System.IO; namespace Aspose.Demo.Pdf.Merger.Controllers { [Route("api/[controller]")] [ApiController] public class MergeController : ControllerBase { private readonly ILogger<MergeController> _logger; private readonly string _storageRootFolder; public MergeController(ILogger<MergeController> logger, IWebHostEnvironment env) { _logger = logger; _storageRootFolder = Path.Combine(env.WebRootPath, "files"); //var license = new License(); //license.SetLicense(@"<path to license>"); } // POST: /api/merge [HttpPost] public IActionResult PostMergeFiles(IEnumerable<string> list) { //TODO: Implement Image to PDF conversion throw new NotImplementedException(); } } }
如您所见,我们的控制器调用HTTP-Post方法来合并文档。现在我们实现此方法。我们合并的想法是将所有页面从一个文档添加到另一个文档。这很简单,因为我们知道Document类包含一个Pages集合,而最后一个具有Add方法。
// POST: /api/merge [HttpPost] public IActionResult PostMergeFiles(IEnumerable<string> list) { var document = new Document(); foreach (var item in list) { var filePath = Path.Combine(_storageRootFolder, item); var pdfDocument = Path.GetExtension(item) switch { ".jpg" => ConvertFromImage(filePath), ".jpeg" => ConvertFromImage(filePath), ".png" => ConvertFromImage(filePath), ".oxps" => new Document(filePath, new XpsLoadOptions()), _ => new Document(filePath) }; document.Pages.Add(pdfDocument.Pages); pdfDocument.Dispose(); } var guid = Guid.NewGuid(); document.Save(Path.Combine(_storageRootFolder, $"{guid}.pdf")); _logger.LogInformation($"The merge result saved as: {guid}"); return Ok(new { filename = guid.ToString() }); } private Document ConvertFromImage(string filePath) { var docStream = new MemoryStream(); var doc = new Document(); var page = doc.Pages.Add(); var image = new Aspose.Pdf.Image { ImageStream = new FileStream(filePath, FileMode.Open, FileAccess.Read) }; page.PageInfo.Margin.Bottom = 0; page.PageInfo.Margin.Top = 0; page.PageInfo.Margin.Left = 0; page.PageInfo.Margin.Right = 0; var imageSize = System.Drawing.Image.FromStream(image.ImageStream).Size; page.PageInfo.Width = imageSize.Width; page.PageInfo.Height = imageSize.Height; page.Paragraphs.Add(image); doc.Save(docStream); return doc; } }
步骤2:实现用于将图像转换为PDF的辅助方法
private Document ConvertFromImage(string filePath) { var docStream = new MemoryStream(); var doc = new Document(); var page = doc.Pages.Add(); var image = new Aspose.Pdf.Image { ImageStream = new FileStream(filePath, FileMode.Open, FileAccess.Read) }; page.PageInfo.Margin.Bottom = 0; page.PageInfo.Margin.Top = 0; page.PageInfo.Margin.Left = 0; page.PageInfo.Margin.Right = 0; var imageSize = System.Drawing.Image.FromStream(image.ImageStream).Size; page.PageInfo.Width = imageSize.Width; page.PageInfo.Height = imageSize.Height; page.Paragraphs.Add(image); doc.Save(docStream); return doc; }
本文示例演示了Aspose.PDF库在ASP.NET Core环境中的正常运行。该应用程序的目的是展示使用.NET Core的Aspose.PDF合并任何文档并将其保存为PDF格式的可能性,并且可能需要对其进行改进。例如,此程序不考虑保存具有相同名称的文件。该问题的可能解决方案是使用具有生成名称的文件夹上载每个文档或使用数据库存储文件。
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@cahobeh.cn