在ASP.NET Core中如何将各种文档合并为PDF?Aspose快速搞定!

在各种业务环境中,将各种文档合并为一个PDF是客户最常问的问题之一。例如,假设您的组织有多个应用程序以XPS和PDF生成特定的文档,使用扫描的图像,并且您的用户希望将其中一些文档合并为一个PDF。

本文演示了如何使用ASP.NET Core框架将多个文档合并到一个PDF中。Aspose.PDF提出了几种使用.NET合并PDF的方法,这些内容在本文中进行了介绍。在本文中,将讨论以下主题:

  • 如何使用ASP.NET Core Web API上传PDF或其他文档;
  • 如何实现简单的Web UI来选择要合并的PDF文件;
  • 如何实现用于合并PDF的简单Web API容器;

在本文中,我们将创建一个简单的ASP.NET Web API应用程序,该应用程序允许我们上载文档,选择2个或更多文件进行合并以及下载结果。

4步教你学会使用Aspose在ASP.NET Core中将各种文档合并为PDF

点击下载最新版Aspose.PDF


实施ASP.NET Core Web App以将各种文档合并为PDF

步骤1:创建一个ASP.NET Core Web应用程序

我们将为此应用程序使用Web应用程序(模型-视图-控制器)模板。

4步教你学会使用Aspose在ASP.NET Core中将各种文档合并为PDF

创建基本应用程序后,我们将需要执行一些其他操作。

  • 为.NET库添加Aspose.PDF作为依赖项(通过Nuget软件包管理器);
  • 添加resumable.js库;
  • 将临时文件和文档的wwwroot文件夹添加到该文件夹(例如files和temp);
  • 在appsettings.json中创建相应的属性
    "Folders": {
        "Files": "files",
        "Temporary" :  "temp" 
    } 

步骤2:实施Web API控制器以管理服务器上的文件

我们的控制器应执行以下操作:

  • 返回具有某些扩展名的文件列表(在本示例中,将仅显示.pdf,.jpg和.oxps文件);
  • 允许按文件名下载文件;
  • 允许通过文件名删除服务器上的文件;
    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库的主要功能是它允许您分块加载文件。因此,我们需要实现一些方法来处理此过程:

  • HTTP GET请求的方法,该方法应检查服务器上是否存在块;
  • HTTP POST请求的方法,该方法应该是服务器上的上传块;
  • 其他辅助方法(用于HTTP OPTIONS请求,合并块等)
    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的单页应用程序。应用程序页面可以分为两个部分:

  • 服务器上的文件部分将使我们可以查看服务器上的文件,下载或删除它们。此外,用户可以通过单击文件名来选择要合并的文档。要获取合并的文档,用户应单击“合并”按钮,合并的文档将显示在文件列表中。
  • “上载文件”部分仅用于上载文件。

由于该网页的代码量很大,因此在此不再显示,我们将完全局限于描述该算法的两个想法。

  • 合并序列中的文件位置存储在与其对应的单元格的data-order属性中。因此,要将文件合并为一个PDF,我们应该获取所有数据顺序,对它们进行排序并发送文件名序列;
  • 要选择/取消选择要合并的文件,请单击文件名。选定的文件标有徽章;

    4步教你学会使用Aspose在ASP.NET Core中将各种文档合并为PDF

以下代码段演示了这两种操作的处理程序:

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格式的可能性,并且可能需要对其进行改进。例如,此程序不考虑保存具有相同名称的文件。该问题的可能解决方案是使用具有生成名称的文件夹上载每个文档或使用数据库存储文件。


如果您有任何疑问或需求,请随时加入Aspose技术交流群(642018183),我们很高兴为您提供查询和咨询。