利用.NET6与Aspose.Words实现高效Word模板导出与PDF转换

news2026/3/14 4:24:39
1. 为什么选择.NET6和Aspose.Words来处理文档如果你正在开发一个需要生成报告、合同、通知函这类正式文档的.NET应用那你肯定遇到过这个头疼的问题怎么才能又快又好地生成格式规范的Word文档并且还能一键转换成PDF自己用Open XML SDK也就是我们常说的DocumentFormat.OpenXml去拼那代码量简直让人望而生畏一个简单的表格对齐或者页眉页脚就能折腾半天。用微软官方的Microsoft.Office.Interop.Word且不说它严重依赖本地安装的Office软件在服务器环境部署就是个噩梦性能和稳定性也完全没法保证。我过去在项目里踩过这些坑后来转向了专业的文档处理库其中Aspose.Words给我的体验是最好的。它就像一个功能齐全的“文档工厂”你只需要告诉它你想要什么样子它就能帮你精确地生产出来。而**.NET 6**作为微软的长期支持LTS版本在性能、跨平台支持和开发体验上都有了质的飞跃用它来搭配Aspose.Words可以说是如虎添翼。简单来说这个组合能帮你解决几个核心痛点告别手动排版你可以先精心设计一个Word模板预留好占位符。程序运行时只需要把数据“灌”进去Aspose.Words会自动处理好所有格式生成专业级的文档。无缝PDF转换生成的Word文档往往还需要以不可编辑的PDF格式分发或存档。Aspose.Words内置了高质量的PDF转换引擎转换后的PDF能最大程度保持原Word文档的版式和字体效果比很多在线转换工具都要好。处理复杂内容你的数据里可能包含从富文本编辑器来的HTML片段比如加粗、斜体、列表、甚至表格。Aspose.Words能直接识别并正确地将这些HTML渲染到Word文档里这功能太实用了。高性能与稳定性完全在服务器内存中操作不依赖任何外部软件非常适合在Web API或后台服务中进行批量文档生成和转换。接下来我就手把手带你从零开始搭建一个基于.NET 6 Web API的文档服务实现从模板加载、数据填充、HTML嵌入、字体设置到PDF转换和文件下载的完整流程。我会把我在实际项目中遇到的“坑”和解决方案都分享出来保证你跟着做就能跑通。2. 项目环境搭建与核心库安装万事开头先搭环境。这里我假设你已经安装了.NET 6 SDK和喜欢的IDE比如Visual Studio 2022或VS Code。首先我们创建一个新的Web API项目。打开命令行执行dotnet new webapi -n WordExportService cd WordExportService项目创建好后我们就要引入今天的主角——Aspose.Words for .NET。打开NuGet包管理器搜索“Aspose.Words”。这里有个非常重要的版本选择问题需要特别注意。Aspose.Words的版本需要与你的.NET运行时兼容。对于.NET 6我们通常选择21.8或更高版本。原始文章中提到使用21.8.0版本这是一个经过验证可稳定运行在.NET 6上的版本。我实测过从21.8到最新的24.x版本在.NET 6上都没有问题。在包管理器控制台执行安装命令Install-Package Aspose.Words -Version 21.8.0关于许可证License的特别说明 Aspose.Words在没有应用有效许可证的情况下运行会在生成的文档页面顶部添加一个评估水印。这对于开发和测试没问题但如果要上线就需要购买正式许可证。网络上流传的一些所谓的“破解激活码”或“密钥”通常涉及修改程序集或使用非法的许可证文件这存在巨大的法律风险和安全风险可能内嵌恶意代码。我强烈建议在开发阶段可以使用评估模式上线前通过Aspose官网购买正版许可证。应用许可证非常简单只需将购买后获得的.lic文件放入项目或在程序启动时加载许可证字符串即可。一个合法的开发者许可证价格对于企业应用来说是完全可以接受的成本它能保障你的项目合规、安全且能获得官方的技术支持。我们会在后面的代码中展示如何应用许可证。3. 核心实战从零编写Word导出功能理论说完我们直接上代码。我会创建一个ExportController并逐步实现一个完整的导出接口。3.1 基础文档创建与段落格式化首先我们引入必要的命名空间并创建一个基础的HTTP POST接口。using Aspose.Words; using Aspose.Words.Saving; using Microsoft.AspNetCore.Mvc; using System.IO; namespace WordExportService.Controllers; [ApiController] [Route(api/[controller])] public class ExportController : ControllerBase { [HttpPost(export-word)] public IActionResult ExportWord([FromBody] ExportRequest request) { // 应用许可证移除水印此处应使用你合法的许可证文件或字符串 // 为了演示这里使用一个伪代码实际应从安全配置中读取 // ApplyLicense(); // 1. 创建一个全新的空白文档 Document doc new Document(); // 2. 创建DocumentBuilder它是我们向文档中添加内容的“画笔” DocumentBuilder builder new DocumentBuilder(doc); // 3. 开始添加内容一个居中的标题 // 插入一个新段落并获取该段落对象以便进行精细控制 Paragraph titlePara builder.InsertParagraph(); titlePara.ParagraphFormat.Alignment ParagraphAlignment.Center; // 段落居中 titlePara.ParagraphFormat.LineSpacingRule LineSpacingRule.Exactly; // 行距规则固定值 titlePara.ParagraphFormat.LineSpacing 28; // 行距28磅 titlePara.ParagraphFormat.SpaceBefore 20; // 段前间距20磅 titlePara.ParagraphFormat.SpaceAfter 20; // 段后间距20磅 // 在段落中创建一个“Run”文本运行并设置其格式 Run titleRun new Run(doc, 员工绩效考核报告); titleRun.Font.Size 22; titleRun.Font.Bold true; titleRun.Font.Name 微软雅黑; // 指定中文字体 titleRun.Font.Color System.Drawing.Color.DarkBlue; // 将设置好格式的Run添加到段落中 titlePara.AppendChild(titleRun); // 移动“画笔”到文档末尾准备写入下一段内容 builder.MoveToDocumentEnd(); // 插入一个换行符相当于回车 builder.Writeln(); // 4. 添加正文内容 builder.ParagraphFormat.Alignment ParagraphAlignment.Left; // 左对齐 builder.ParagraphFormat.FirstLineIndent 21; // 首行缩进21磅约2个字符 builder.Font.Size 12; builder.Font.Name 宋体; builder.Write(尊敬的部门领导); builder.Writeln(); builder.Write( 现将本季度员工「 request.EmployeeName 」的绩效数据汇总如下。本报告生成时间 DateTime.Now.ToString(yyyy年MM月dd日)); // ... 后续可以继续使用builder插入表格、图片等 // 5. 将文档保存到内存流准备返回给前端 using (MemoryStream memoryStream new MemoryStream()) { doc.Save(memoryStream, SaveFormat.Docx); memoryStream.Position 0; // 将流的位置重置到开头 // 调用方法返回文件流下一节会详细讲 return File(memoryStream.ToArray(), application/vnd.openxmlformats-officedocument.wordprocessingml.document, ${request.EmployeeName}_绩效报告.docx); } } } public class ExportRequest { public string EmployeeName { get; set; } }这段代码演示了创建文档、使用DocumentBuilder进行段落和字体级格式控制的完整过程。你可以看到通过ParagraphFormat和Font属性我们能控制几乎所有的排版细节。3.2 处理HTML富文本内容在实际业务中用户输入的备注、描述等信息常常是带格式的HTML。Aspose.Words的DocumentBuilder.InsertHtml方法完美解决了这个问题。假设前端提交的数据中有一个字段PerformanceComment是HTML字符串例如“p该员工本季度strong表现突出/strong超额完成u所有KPI指标/u。/pulli优点沟通能力强/lili待改进文档撰写细节/li/ul”。我们可以在代码中这样插入// 假设我们已经有了builder对象并设置好了正文的默认字体如上面的宋体12号 builder.Writeln(); // 先换行 builder.Write(绩效评语); builder.Writeln(); // 关键步骤插入HTML // InsertHtml方法的第二个参数为true表示继承当前DocumentBuilder的格式字体、大小等 builder.InsertHtml(request.PerformanceComment, true);当InsertHtml的第二个参数设置为true时HTML内容会继承builder当前的字体设置宋体12号这能保证文档整体风格一致。插入后Word文档中会正确显示加粗的“表现突出”、带下划线的“所有KPI指标”以及项目列表。这个功能极大地简化了复杂内容渲染的工作。3.3 应用字体与解决中文乱码/缺失问题字体是中文文档处理中最容易踩的坑。如果你指定的字体如“创艺简标宋”在运行服务的服务器上不存在Aspose.Words可能会回退到默认字体导致生成的文档与预期样式不符或者在转换为PDF时出现字体缺失、显示为方框的问题。解决方案是字体嵌入。我们可以在生成PDF时告诉Aspose.Words将文档中使用到的所有字体子集嵌入到PDF文件中。// 接上文的文档生成代码假设我们已经有了最终的Document对象 doc // 现在要将其转换为PDF PdfSaveOptions pdfSaveOptions new PdfSaveOptions(); pdfSaveOptions.SaveFormat SaveFormat.Pdf; // 最关键的两个字体相关设置 pdfSaveOptions.EmbedFullFonts true; // 嵌入完整字体或字体子集 pdfSaveOptions.EmbedCoreFonts false; // 通常设为false避免嵌入标准字体以减小文件大小 // 如果你知道服务器上字体可能缺失还可以指定字体替换规则 // 例如当“创艺简标宋”缺失时用“微软雅黑”替代 // pdfSaveOptions.FontSettings new FontSettings(); // pdfSaveOptions.FontSettings.SubstitutionSettings.DefaultFontSubstitution.DefaultFontName Microsoft YaHei; // 保存为PDF string pdfFilePath Path.Combine(outputFolder, output.pdf); doc.Save(pdfFilePath, pdfSaveOptions);设置EmbedFullFonts true后生成的PDF文件会包含所需的字体信息无论在哪台电脑上打开都能确保文字按设计显示。这虽然会稍微增加PDF文件的大小但对于保证文档显示一致性是至关重要的。4. 高级功能PDF转换与文件交付文档生成好了接下来就是如何把它交给用户。4.1 高质量的Word转PDF上面已经提到了PDF转换的核心设置——字体嵌入。除此之外还有一些其他实用选项可以优化PDF输出PdfSaveOptions options new PdfSaveOptions { EmbedFullFonts true, // 设置PDF兼容性级别例如PDF/A标准用于长期归档 // Compliance PdfCompliance.PdfA1a, // 图像压缩和质量控制 ImageCompression PdfImageCompression.Jpeg, JpegQuality 90, // 文档属性 DisplayDocTitle true, CustomPropertiesExport PdfCustomPropertiesExport.Standard }; doc.Save(final_output.pdf, options);4.2 前端直接下载与后端文件流返回在Web API中我们通常不把文件保存在服务器磁盘上而是直接生成到内存流中通过HTTP响应返回给浏览器。ControllerBase.File方法非常适合这个场景。我们完善一下之前接口的返回部分[HttpPost(export-word)] public IActionResult ExportWord([FromBody] ExportRequest request) { // ... 前面的文档生成代码最终得到 Document doc 对象 ... string fileName ${request.EmployeeName}_绩效报告_{DateTime.Now:yyyyMMdd}.docx; // 对文件名进行UTF-8编码确保中文文件名在浏览器中不会乱码 string encodedFileName Uri.EscapeDataString(fileName); using (MemoryStream memoryStream new MemoryStream()) { doc.Save(memoryStream, SaveFormat.Docx); byte[] fileBytes memoryStream.ToArray(); // 关键通过File方法返回文件流 // 第一个参数文件内容的字节数组 // 第二个参数MIME类型对于.docx文件是固定的 // 第三个参数浏览器下载时建议的文件名 return File(fileBytes, application/vnd.openxmlformats-officedocument.wordprocessingml.document, fileName); } }前端这里以原生JavaScript的Fetch API为例可以这样调用并触发下载async function downloadReport(userId) { const response await fetch(/api/export/export-word, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ employeeName: 张三 }) }); if (!response.ok) { alert(导出失败); return; } // 从响应头中获取文件名如果后端设置了的话 const contentDisposition response.headers.get(content-disposition); let filename report.docx; if (contentDisposition) { const match contentDisposition.match(/filename\*?[]?(?:UTF-\d[]*)?([^;])[]?/i); if (match match[1]) { filename decodeURIComponent(match[1]); } } // 将响应转换为Blob并创建下载链接 const blob await response.blob(); const downloadUrl window.URL.createObjectURL(blob); const a document.createElement(a); a.href downloadUrl; a.download filename; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(downloadUrl); // 释放内存 }4.3 批量导出与ZIP压缩打包对于需要一次性导出多份报告的场景比如导出整个部门的考核报告逐个下载非常低效。更好的做法是在服务器端将所有Word文档打包成一个ZIP文件让用户一次下载。我们需要引入System.IO.Compression命名空间。using System.IO.Compression; [HttpPost(export-batch-zip)] public IActionResult ExportBatchZip([FromBody] BatchExportRequest request) { // 假设request.EmployeeIds是一个员工ID列表 ListExportedFile fileList new ListExportedFile(); foreach (var employeeId in request.EmployeeIds) { // 1. 为每个员工生成Word文档复用之前的单个导出逻辑 Document doc GenerateWordDocumentForEmployee(employeeId); // 这是一个假设的封装方法 string fileName ${employeeId}_Report.docx; using (MemoryStream ms new MemoryStream()) { doc.Save(ms, SaveFormat.Docx); // 将文件名和字节数据存入列表 fileList.Add(new ExportedFile { Name fileName, Content ms.ToArray() }); } } // 2. 创建ZIP压缩包 using (MemoryStream zipStream new MemoryStream()) { using (ZipArchive archive new ZipArchive(zipStream, ZipArchiveMode.Create, true)) { foreach (var file in fileList) { // 在ZIP包中创建一个条目即一个文件 ZipArchiveEntry entry archive.CreateEntry(file.Name); using (Stream entryStream entry.Open()) using (BinaryWriter writer new BinaryWriter(entryStream)) { writer.Write(file.Content); } } } // 3. 返回ZIP文件流 zipStream.Position 0; string zipFileName $部门绩效报告_{DateTime.Now:yyyyMMddHHmm}.zip; return File(zipStream.ToArray(), application/zip, zipFileName); } } // 辅助类用于存储生成的文件 public class ExportedFile { public string Name { get; set; } public byte[] Content { get; set; } } public class BatchExportRequest { public Liststring EmployeeIds { get; set; } }这个流程清晰明了循环生成每个文档 - 存入内存 - 全部添加到ZIP归档 - 一次性返回给前端。这能极大提升用户体验和服务器效率。5. 性能优化与避坑指南在实际生产环境中使用有几个点需要特别注意它们直接关系到系统的稳定性和效率。第一许可证管理。千万不要把许可证文件或密钥字符串硬编码在代码里。最佳实践是将其放在appsettings.json或环境变量中在程序启动时比如在Program.cs的Main或Startup中一次性加载。// 在Program.cs或启动配置类中 var licenseContent Configuration[Aspose:LicenseBase64String]; if (!string.IsNullOrEmpty(licenseContent)) { try { using (var licenseStream new MemoryStream(Convert.FromBase64String(licenseContent))) { var license new License(); license.SetLicense(licenseStream); Console.WriteLine(Aspose.Words 许可证已应用。); } } catch (Exception ex) { // 记录日志但不要阻止应用启动评估模式也可运行 Console.WriteLine($应用Aspose许可证失败: {ex.Message}); } }第二资源释放。Document和DocumentBuilder对象使用了非托管资源。虽然它们实现了IDisposable但在典型的“创建-保存-丢弃”场景中不手动调用Dispose()通常也能被GC正常回收。然而在高并发批量处理时为了更确定地控制内存建议使用using语句包裹或者确保在finally块中释放。// 推荐在确定的代码块内使用 using (Document doc new Document()) using (DocumentBuilder builder new DocumentBuilder(doc)) { // 操作文档... doc.Save(outputStream, SaveFormat.Docx); } // 离开作用域自动释放资源第三字体缓存。Aspose.Words在首次使用某种字体或进行PDF转换时可能会执行字体扫描和缓存。这个过程在第一次调用时可能会有一些延迟。可以在应用启动后预先触发一次简单的文档操作来“预热”这个缓存避免第一个用户请求响应过慢。第四异步处理。对于非常耗时的文档生成任务比如生成数百页的报表可以考虑将操作放入后台队列如Hangfire、Azure Queue通过WebSocket或轮询通知前端任务完成并提供下载链接而不是让HTTP请求长时间等待。踩过几次坑之后我的经验是在开发阶段就使用真实的、数据量大的用例进行测试关注服务器在文档生成期间的内存和CPU使用情况对于字体问题坚持在生成PDF时嵌入字体。把这些细节处理好你构建的文档服务就会非常稳健可靠。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2409911.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…