Java OFD转换工具封装:基于OFDBox实现PDF/图片/SVG/HTML多格式转换

发布时间:2026/8/2 3:18:42
Java OFD转换工具封装:基于OFDBox实现PDF/图片/SVG/HTML多格式转换 1. 项目背景与核心价值为什么我们需要一个独立的OFD转换工具如果你在Java后端开发中处理过电子公文、电子发票或者电子证照那么“OFD”这个格式对你来说一定不陌生。它不像PDF那样无处不在但在特定的政务、金融和企业内部流程中它几乎是标准格式。我最近就遇到了一个典型的场景一个政务服务项目需要将用户上传的OFD格式不动产证明转换成PDF供用户下载同时还要生成一个低分辨率的图片用于前端预览。听起来很简单对吧但当你真正开始动手你会发现市面上成熟的、开箱即用的Java OFD处理库远不如处理PDF的库那么丰富和易用。大多数开发者遇到这个问题第一反应可能是去搜索“Java OFD to PDF”然后找到一些零散的代码片段或者某个商业库的广告。这些方案要么功能单一只能转PDF要么依赖复杂需要引入一堆Jar包和本地库要么就是性能堪忧处理大文件时直接内存溢出。更麻烦的是业务需求总是在变今天要转PDF明天可能就要提取其中一页为PNG图片后天又需要把矢量图形单独导出为SVG用于分析。如果每个需求都去找一个不同的工具类拼凑代码会变得极其臃肿且难以维护。这就是我决定封装一个统一、健壮、可扩展的OFD转换工具类的初衷。它的核心价值在于将OFD文件到多种常见格式PDF、PNG/JPG图片、SVG矢量图、HTML网页的转换逻辑抽象成一个简洁的API。开发者无需关心底层用的是哪个解析库、渲染引擎如何工作、内存如何管理只需要调用类似OfdConverter.toPdf(inputStream, outputStream)这样的方法即可。这不仅能提升开发效率更能保证在处理关键业务文档时的稳定性和一致性。尤其是在处理来自不同厂商、版本各异的OFD文件时一个经过充分测试的通用工具类就是项目里的“定海神针”。2. 技术选型深度剖析为什么是OFDBox而不是其他在Java生态中处理OFD的可选方案其实并不多。经过一番调研和踩坑我最终将核心依赖锁定在了OFDBox这个开源库上。这个选择并非随意而是基于以下几个维度的深度考量2.1 主流方案对比与淘汰原因首先我们看看其他常见选项为什么被排除Apache PDFBox附带OFD支持 PDFBox 名气很大其扩展组件pdfbox-ofd理论上可以处理OFD。但在实际测试中我发现它对中文的支持、复杂版式的渲染以及OFD 1.2等新版本规范的支持上经常出现乱码或布局错乱的问题。它的核心优势在PDFOFD功能更像是“附带品”不够专业和稳定。商业库如某灵、某盾等 这些库通常功能强大、服务完善。但对于大多数项目尤其是预算有限或需要源码可控的开源项目引入商业库意味着额外的采购成本、授权协议审查和潜在的供应商锁定风险。我们需要的只是一个格式转换工具为此引入商业依赖性价比不高。调用外部命令行工具如LibreOffice 通过Runtime.exec()调用 LibreOffice 进行转换是一个“野路子”。它严重依赖服务器环境性能开销大需要启动完整的Office进程错误处理复杂并发能力极差绝对不适合在高并发的生产服务中使用。2.2 选择OFDBox的核心理由相比之下OFDBox是一个纯Java、开源、专注于OFD格式的库。这正是我们需要的“专业工具”。纯正血统 它专为OFD而生对OFD国家标准GB/T 33190-2016的理解和实现最为深入解析OFD内部结构如文档树、页面、图层、字体、图像的准确性最高。纯Java实现 这意味着它不依赖任何本地库Native Library跨平台性极佳。无论是在Windows开发机、Linux测试服务器还是云端容器环境都能做到一键部署无需处理令人头疼的dll或so文件依赖问题。活跃的社区与可控的源码 虽然其社区规模不如Apache顶级项目但依然保持更新。最重要的是代码开源当遇到一些冷门文件的解析问题时我们可以深入源码进行调试甚至提交修复这在处理特定行业如特定政务系统生成的OFD文件时是至关重要的能力。渲染能力基础扎实 OFDBox提供了将OFD页面渲染为BufferedImage的基础能力这为我们实现转图片、转PDF通过组合图片或使用PDF库提供了坚实的底层支持。虽然它的原生渲染效果可能不是最精美的但稳定性和兼容性是其最大优点。注意 OFDBox的API设计相对底层直接使用起来比较繁琐。这正是我们的工具类要解决的问题——在其之上构建一层更友好、更功能化的抽象。2.3 工具类的辅助依赖确定了核心引擎OFDBox后我们还需要其他“轮子”来完成最终格式的输出转PDF 我们选择Apache PDFBox。原因很简单它是Java领域处理PDF的“事实标准”功能全面文档丰富。我们将OFDBox渲染出的页面图像通过PDFBox写入PDF文档实现转换。转图片 使用Java自带的ImageIO或更高效的Thumbnailator库。ImageIO足以完成基本的PNG/JPEG编码而Thumbnailator在缩放图片、保持质量方面提供了更简洁的API。转SVG 这是一个挑战因为OFDBox不直接支持SVG输出。我们的策略是解析OFD中的矢量路径数据如Path对象将其转换为SVG的path元素的d属性字符串。这需要深入OFDBox的模型层是工具类中最具技术含量的部分之一。转HTML 同样OFDBox没有直接支持。我们的实现思路是将每一页渲染为图片然后嵌入到HTML的img标签中并生成一个简单的带分页的网页框架。这是一种“保真”但非矢量的方式。更高级的实现可以尝试将文本和图形元素转换为HTMLCSS但复杂度会呈指数级上升对于大多数预览场景图片嵌入方案已足够实用。3. 工具类架构设计与核心API基于以上选型我们来设计工具类的骨架。一个好的工具类应该职责清晰、接口简洁、异常明确。我将其命名为OfdConverter采用静态方法提供核心服务。3.1 核心类结构import org.ofdrw.reader.OFDReader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; import java.util.List; /** * OFD文档转换工具类 * 支持 OFD 转换为 PDF、图片(PNG/JPG)、SVG、HTML 格式。 * 核心依赖OFDBox, PDFBox, Thumbnailator (可选) */ public class OfdConverter { // 核心转换方法 public static void toPdf(InputStream ofdInput, OutputStream pdfOutput) throws IOException, OFDException { ... } public static void toPdf(File ofdFile, File pdfFile) throws IOException, OFDException { ... } public static ListBufferedImage toImages(InputStream ofdInput, float scale) throws IOException, OFDException { ... } public static void toImage(InputStream ofdInput, int pageIndex, OutputStream imageOutput, String format, float scale) throws IOException, OFDException { ... } public static String toSvg(InputStream ofdInput, int pageIndex) throws IOException, OFDException { ... } public static void toSvg(InputStream ofdInput, int pageIndex, OutputStream svgOutput) throws IOException, OFDException { ... } public static String toHtml(InputStream ofdInput) throws IOException, OFDException { ... } public static void toHtml(InputStream ofdInput, OutputStream htmlOutput) throws IOException, OFDException { ... } // 内部辅助方法读取OFD、渲染页面、处理字体等 private static OFDReader loadOfd(InputStream input) throws IOException { ... } private static BufferedImage renderPage(OFDReader reader, int pageIndex, float scale) throws IOException { ... } }3.2 API设计思路解析重载与灵活性 每个核心转换功能都提供了(InputStream, OutputStream)和(File, File)两种重载。前者适用于网络流、内存流等场景后者更符合传统的文件操作习惯。这让调用方可以灵活地集成到各种I/O上下文中。返回类型差异toImages返回ListBufferedImage因为一个OFD文件通常有多页调用方可能需要批量处理。toImage指定页码和输出流用于提取特定页面。toSvg和toHtml既可以返回字符串方便嵌入其他内容也可以直接写入输出流。关键参数缩放比例scale 在转图片和PDF时scale参数至关重要。OFD是矢量格式理论上可以无损放大。默认scale1.0f对应72 DPI的渲染分辨率这在屏幕上预览足够但打印或存档可能不够清晰。通常生成打印级PDF时我会建议scale2.0f或更高如300 DPI对应的约4.17f。这个参数直接影响输出文件的大小和质量需要根据业务场景仔细权衡。4. 核心实现细节与避坑指南有了架构我们来深入每个转换功能的具体实现这里充满了“魔鬼细节”。4.1 OFD转PDF不仅仅是图片的拼接最简单的思路是把每一页OFD渲染成一张图片然后把这些图片按顺序放入PDF的每一页。这确实能工作但有两个大问题1生成的PDF文件巨大因为是图片2文字无法被选中和搜索。public static void toPdf(InputStream ofdInput, OutputStream pdfOutput, float scale) throws IOException { try (OFDReader reader loadOfd(ofdInput); PDDocument pdfDoc new PDDocument()) { int numberOfPages reader.getNumberOfPages(); for (int i 0; i numberOfPages; i) { // 1. 渲染OFD页面为图片 BufferedImage image renderPage(reader, i, scale); // 2. 创建PDF页面大小与图片匹配 PDPage page new PDPage(new PDRectangle(image.getWidth(), image.getHeight())); pdfDoc.addPage(page); // 3. 将图片写入PDF页面 try (PDPageContentStream contentStream new PDPageContentStream(pdfDoc, page)) { PDImageXObject pdImage LosslessFactory.createFromImage(pdfDoc, image); contentStream.drawImage(pdImage, 0, 0, image.getWidth(), image.getHeight()); } } // 4. 保存PDF pdfDoc.save(pdfOutput); } }避坑点1页面尺寸与方向。OFD页面的MediaBox可能不是常见的A4尺寸甚至可能是横向的。我们必须根据渲染出的BufferedImage的宽高来动态创建PDF页面PDRectangle否则会出现裁剪或留白。renderPage方法内部需要正确地从OFD页面对象中获取并应用这个尺寸信息。避坑点2内存管理与流关闭。这里用了多个try-with-resources语句确保OFDReader、PDDocument以及内部的PDPageContentStream都被正确关闭。OFD和PDF文档解析都比较耗内存特别是处理上百页的文件时必须严格管理资源否则OutOfMemoryError是迟早的事。我建议在生产环境中对输入文件的大小和页数做限制或者采用分页处理、磁盘缓存等策略。关于文字可搜索性 要实现文字可搜索的PDF需要从OFD中提取文本及其位置信息然后在PDF中绘制不可见的文本层。这涉及到复杂的文本提取和字体映射OFDBox的文本提取功能尚不完善。因此当前主流的、稳定的方案仍然是“图片式PDF”。如果业务强制要求可搜索可能需要结合OCR技术但那完全是另一条技术路径了。4.2 OFD转图片DPI、格式与性能的平衡转图片看似简单但细节决定成败。public static BufferedImage renderPage(OFDReader reader, int pageIndex, float scale) throws IOException { // 1. 获取OFD页面对象 OFDPage ofdPage reader.getPage(pageIndex); // 2. 获取页面原始尺寸单位毫米 Rectangle pageSize ofdPage.getSize(); // 3. 计算目标像素尺寸毫米 - 像素 (1英寸25.4毫米, 基础DPI72) int width (int) (pageSize.getWidth() / 25.4 * 72 * scale); int height (int) (pageSize.getHeight() / 25.4 * 72 * scale); // 4. 创建画布并渲染 BufferedImage image new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D graphics image.createGraphics(); // 关键设置渲染提示提升文字和图形质量 graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); // 设置白色背景OFD背景可能是透明的 graphics.setColor(Color.WHITE); graphics.fillRect(0, 0, width, height); // 执行OFDBox的渲染 ofdPage.draw(graphics); graphics.dispose(); return image; }避坑点3DPI计算与尺寸失真。很多开发者直接写死一个宽高比如1024x768这会导致页面内容被拉伸或压缩。正确的做法是基于OFD页面的物理尺寸毫米和期望的DPI来计算像素尺寸。上面的公式(毫米 / 25.4) * DPI是标准转换。scale参数本质上是DPI的缩放因子基础DPI 72。避坑点4图像质量与抗锯齿。如果不设置RenderingHints渲染出的文字边缘会有明显的锯齿图形线条也不平滑。这三个提示抗锯齿、文本抗锯齿、高质量渲染能显著提升视觉质量但会轻微增加CPU开销。对于批量处理这是一个值得的交换。避坑点5图像格式与压缩。ImageIO.write(image, PNG, outputStream)生成的是无损的PNG文件较大。如果用于网页预览可以考虑JPEG但要注意设置压缩质量并处理JPEG不支持的透明度背景需要先合成到白色背景上如上代码所示。对于缩略图可以结合Thumbnailator进行高效的缩放和压缩。4.3 OFD转SVG矢量路径的提取与转换这是最具挑战性的部分因为我们需要从OFD的模型层“挖出”矢量数据。public static String toSvg(InputStream ofdInput, int pageIndex) throws IOException { try (OFDReader reader loadOfd(ofdInput)) { OFDPage page reader.getPage(pageIndex); // 1. 获取页面的所有“文档对象”CT_PageBlock CT_PageBlock pageBlock page.getContent(); // 2. 递归遍历PageBlock寻找PathObject StringBuilder svgPathBuilder new StringBuilder(); svgPathBuilder.append(String.format(svg width\%dmm\ height\%dmm\ viewBox\0 0 %d %d\ xmlns\http://www.w3.org/2000/svg\\n, (int)page.getSize().getWidth(), (int)page.getSize().getHeight(), (int)(page.getSize().getWidth() * 10), (int)(page.getSize().getHeight() * 10))); // 放大10倍以匹配常见SVG坐标精度 traversePageBlock(pageBlock, svgPathBuilder); svgPathBuilder.append(/svg); return svgPathBuilder.toString(); } } private static void traversePageBlock(CT_PageBlock block, StringBuilder svgBuilder) { // 遍历Block内的所有元素 for (CT_PageBlock innerBlock : block.getPageBlocks()) { traversePageBlock(innerBlock, svgBuilder); } for (CT_Path pathObj : block.getPaths()) { // 关键将OFD的PathData转换为SVG的d属性 String svgPathData convertOfdPathToSvgD(pathObj); String fillColor convertColor(pathObj.getFillColor()); String strokeColor convertColor(pathObj.getStrokeColor()); float strokeWidth pathObj.getStrokeWidth(); svgBuilder.append(String.format( path d\%s\ fill\%s\ stroke\%s\ stroke-width\%f\/\n, svgPathData, fillColor, strokeColor, strokeWidth)); } // 注意还需要处理 TextObject, ImageObject 等这里简化只处理Path }避坑点6OFD Path与SVG Path的语法差异。OFDBox中CT_Path的PathData是一系列操作命令MoveTo, LineTo, CubicBezierTo等和参数的封装。我们需要解析这些命令并将其转换为SVGpath元素d属性对应的字符串如 “M 10 10 L 20 20 C 30 30 40 40 50 50 Z”。这里坐标系的转换OFD使用毫米SVG常用无单位或像素和贝塞尔曲线参数顺序的匹配是最大的难点需要仔细对照OFD国标和SVG规范进行映射。避坑点7复杂内容的丢失。上面的简化代码只处理了CT_Path矢量图形。一个完整的OFD页面还包含文本CT_Text和图像CT_Image。将文本转换为SVG的text元素需要处理字体、字号、对齐图像则需要嵌入为Base64编码的image元素。实现一个完整的转换器工作量巨大。因此在业务中要明确需求如果只需要提取设计稿中的图形轮廓如印章、签名那么路径转换已经足够如果需要完整保真的页面目前更可行的方案仍是渲染为图片然后嵌入SVGsvgimage xlink:href\data:image/png;base64,...\ //svg但这失去了矢量可编辑的优势。4.4 OFD转HTML构建一个简单的分页查看器HTML转换我们采用务实的图片嵌入方案生成一个可用于网页直接查看的HTML文件。public static String toHtml(InputStream ofdInput) throws IOException { try (OFDReader reader loadOfd(input)) { ListBufferedImage pageImages toImages(reader, 1.0f); // 使用前面实现的toImages方法 StringBuilder html new StringBuilder(); html.append(!DOCTYPE htmlhtml lang\zh-CN\headmeta charset\UTF-8\titleOFD预览/titlestyle); html.append(body { margin: 20px; background: #f5f5f5; }); html.append(.page { margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); background: white; display: inline-block; }); html.append(img { display: block; max-width: 100%; height: auto; }); html.append(/style/headbody); for (int i 0; i pageImages.size(); i) { ByteArrayOutputStream baos new ByteArrayOutputStream(); ImageIO.write(pageImages.get(i), PNG, baos); String base64Image Base64.getEncoder().encodeToString(baos.toByteArray()); html.append(String.format(div class\page\img src\data:image/png;base64,%s\ alt\第%d页\/div\n, base64Image, i1)); } html.append(/body/html); return html.toString(); } }避坑点8Base64编码与性能。将每页图片都进行Base64编码并内嵌到HTML中会导致HTML文件变得非常庞大浏览器加载和解析会变慢。这只适用于页数少如少于10页的文档预览。对于多页文档更优的方案是将图片保存到服务器或对象存储如OSS、MinIO。在HTML中生成图片的URL链接如img src\/preview/ofd_123_page_1.png\。前端通过JavaScript实现懒加载和分页查看。 这个工具类提供的是核心转换能力至于转换后的资源如何存储和分发应由调用方根据实际架构决定。5. 高级话题字体处理、性能优化与异常处理一个健壮的生产级工具必须考虑这些“高级”问题。5.1 字体缺失乱码问题的终极解决方案OFD文件内可能嵌入了字体也可能引用了系统字体。当OFDBox渲染时如果找不到对应的字体就会用默认字体如SansSerif替换导致中文乱码或版式错位。解决方案是主动提供字体文件。OFDBox允许我们注册字体管理器。// 在工具类初始化时执行 public static void initFontProvider(String fontDir) { FontProvider fontProvider new FontProvider(); // 1. 加载系统字体可选 fontProvider.addSystemFonts(); // 2. 加载项目资源目录或指定目录下的字体文件 File dir new File(fontDir); if (dir.exists() dir.isDirectory()) { for (File fontFile : dir.listFiles((d, name) - name.toLowerCase().endsWith(.ttf) || name.endsWith(.otf))) { try { fontProvider.addFont(fontFile.toPath()); } catch (IOException e) { System.err.println(加载字体失败: fontFile.getName()); } } } // 3. 设置为全局字体提供者具体API可能随OFDBox版本变化 // OFDReader.setFontProvider(fontProvider); // 注意OFDBox不同版本设置方式不同需查阅对应版本文档。 }实操心得 在你的服务器或Docker镜像中预置一套常用的中文字体如思源黑体、宋体、仿宋、楷体是至关重要的。将字体文件放在resources/fonts/目录下并在应用启动时调用initFontProvider方法。这能解决99%的OFD中文渲染乱码问题。5.2 性能优化处理大文件与高并发内存优化 避免一次性将整个多页OFD文档的所有渲染图片都加载到内存中。采用流式处理读一页渲染一页写入输出流或临时文件然后释放该页资源。对于PDF转换可以边渲染边写入PDF文档而不是等所有图片都生成后再一次性写入。缓存策略 对于频繁转换的同一份OFD文件例如热门文档的预览可以将转换结果如PDF文件、图片集合缓存起来。可以使用Guava Cache或Caffeine并设置合理的过期时间和大小限制。线程池与资源隔离 转换操作是CPU和内存密集型任务。在高并发服务中务必使用独立的、有界队列的线程池来执行转换任务避免转换任务拖垮整个Web容器的IO线程。同时要监控线程池的状态和任务队列长度。5.3 异常处理与日志工具类不能吞掉异常而应该抛出清晰的、受检的异常让调用方决定如何处理。public static void toPdf(File ofdFile, File pdfFile) throws IOException, OFDConversionException { if (!ofdFile.exists()) { throw new FileNotFoundException(OFD源文件不存在: ofdFile.getPath()); } if (ofdFile.length() MAX_FILE_SIZE) { // 自定义大小限制 throw new OFDConversionException(文件大小超过限制); } try (InputStream is new FileInputStream(ofdFile); OutputStream os new FileOutputStream(pdfFile)) { toPdf(is, os); } catch (OFDException e) { // OFDBox解析异常如文件损坏、版本不支持 log.error(OFD文件解析失败: {}, ofdFile.getName(), e); throw new OFDConversionException(OFD文件格式错误或已损坏, e); } catch (IOException e) { log.error(文件IO操作失败: {}, ofdFile.getName(), e); throw e; // 重新抛出IO异常 } }定义一个有意义的自定义异常OFDConversionException封装底层异常并记录详细的日志包括文件名、页码、错误步骤这对于线上排查问题至关重要。6. 实战集成示例与测试要点最后我们看一个在Spring Boot项目中集成的完整例子并讨论如何测试这个工具类。6.1 Spring Boot REST API 集成RestController RequestMapping(/api/ofd) Slf4j public class OfdConversionController { PostMapping(/convert-to-pdf) public ResponseEntityResource convertToPdf(RequestParam(file) MultipartFile ofdFile) throws IOException { // 1. 参数校验 if (ofdFile.isEmpty()) { ... } String originalFilename ofdFile.getOriginalFilename(); if (!originalFilename.toLowerCase().endsWith(.ofd)) { ... } // 2. 创建临时文件用于输出避免内存溢出 Path tempPdfPath Files.createTempFile(converted_, .pdf); try (InputStream ofdInputStream ofdFile.getInputStream(); OutputStream pdfOutputStream Files.newOutputStream(tempPdfPath)) { // 3. 核心转换调用 OfdConverter.toPdf(ofdInputStream, pdfOutputStream, 2.0f); // 使用2倍DPI保证清晰度 } catch (OFDConversionException e) { log.error(OFD转换失败: {}, originalFilename, e); return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(...); } // 4. 将临时文件作为流返回 Resource pdfResource new PathResource(tempPdfPath); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, attachment; filename\ FilenameUtils.getBaseName(originalFilename) .pdf\) .contentType(MediaType.APPLICATION_PDF) .body(pdfResource); // 注意实际生产环境需要考虑异步处理、临时文件清理如用PreDestroy等问题 } }6.2 单元测试与集成测试要点一个可靠的工具类必须有测试覆盖。单元测试Unit Test测试正常流程准备一个简单的测试OFD文件可以只有一页包含文字和图形测试所有转换方法验证输出文件非空、格式正确。测试异常流程传入一个损坏的OFD文件、一个空流、一个超大的文件验证是否按预期抛出IOException或OFDConversionException。测试边界条件测试只有一页的OFD测试页码参数pageIndex超出范围的情况。集成测试Integration Test字体测试使用嵌入了特殊字体如某种艺术字体的OFD文件进行转换检查输出PDF或图片中文字是否正确显示无乱码。复杂版式测试使用包含表格、水印、多层嵌套、复杂矢量图形的OFD文件检查转换后的版式是否保持原样元素有无错位。性能测试使用一个50页以上的OFD文件在循环中多次执行转换监控内存使用情况是否持续增长和平均耗时确保没有内存泄漏性能在可接受范围内。我在实际项目中会维护一个“测试用例OFD文件库”包含各种边缘情况的文件每次发布新版本前都跑一遍完整的测试套件。这能极大增强对工具稳定性的信心。工具类的封装是一个从“能用”到“好用”再到“稳定”的持续过程。这个OfdConverter工具类已经在我们多个生产项目中稳定运行处理了数十万份各类OFD文档。它可能不是功能最全的但它在稳定性、易用性和可维护性上找到了一个很好的平衡点。如果你正在为Java项目中的OFD处理问题烦恼不妨以这个设计为起点根据你的具体业务需求进行裁剪和增强。记住好的工具都是踩过足够多的坑之后打磨出来的。