diff --git a/src/YhCore.Service/Excel/ExcelHelper.cs b/src/YhCore.Service/Excel/ExcelHelper.cs new file mode 100644 index 0000000..ab86c2a --- /dev/null +++ b/src/YhCore.Service/Excel/ExcelHelper.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using OfficeOpenXml; +using OfficeOpenXml.Style; + +namespace YhCore.Service.Excel +{ + /// + /// EPPlus 多 Sheet 导出(移植自旧 Bsch.Common/DocPrint/ExcelHelper, + /// 保留 RenderToExcelBytes + ExportDynamicSheet:动态列、大标题、自定义列宽、打印优化)。 + /// 注意:EPPlus 实际产出 xlsx 字节流,旧系统文件名沿用 .xls,Excel 可正常打开,保持原契约。 + /// + public class ExcelHelper + { + /// 导出多 Sheet Excel 字节流(listdt/listsheetname/listtitle 按下标一一对应) + public static byte[] RenderToExcelBytes(string fileName, List listdt, List listsheetname, List listtitle, List listcolwidth = null) + { + try + { + var package = new ExcelPackage(); + int n = 0; + foreach (var dt in listdt) + { + string sheetname = "Sheet" + (n + 1).ToString(); + if (n < listsheetname.Count) sheetname = listsheetname[n]; + string title = ""; + if (n < listtitle.Count) title = listtitle[n]; + var ws1 = package.Workbook.Worksheets.Add(sheetname); + ExportDynamicSheet(ws1, dt, title, "A", listcolwidth); + n++; + } + return package.GetAsByteArray(); + } + catch (Exception) { return Array.Empty(); } + } + + #region 动态列 Sheet 导出(通用方法) + + /// + /// 导出 动态列 + 自定义表头 + 自定义页码 + 打印优化 + /// + public static void ExportDynamicSheet(ExcelWorksheet ws, DataTable dt, string sheetTitle, string printMark, List listcolwidth = null) + { + // -------------------------- 行定义 -------------------------- + int titleRow = 1; // 大标题行 + int headerRow = 2; // 列标题行 + int dataStartRow = 3; // 数据开始行 + + int colCount = dt.Columns.Count; + int rowCount = dt.Rows.Count; + + // -------------------------- 1. 自定义大标题(合并行) -------------------------- + if (sheetTitle != "") + { + ws.Cells[titleRow, 1].Value = sheetTitle; + ws.Cells[titleRow, 1, titleRow, colCount].Merge = true; // 合并单元格 + + // 标题样式 + var titleStyle = ws.Cells[titleRow, 1].Style; + titleStyle.Font.Size = 12; + titleStyle.Font.Bold = true; + titleStyle.HorizontalAlignment = ExcelHorizontalAlignment.Center; + titleStyle.VerticalAlignment = ExcelVerticalAlignment.Center; + ws.Row(titleRow).Height = 32; + } + else + { + headerRow = 1; // 列标题行 + dataStartRow = 2; // 数据开始行 + } + + // -------------------------- 2. 动态列 自动填充表头 -------------------------- + for (int c = 0; c < colCount; c++) + { + ws.Cells[headerRow, c + 1].Value = dt.Columns[c].ColumnName; + } + + // -------------------------- 3. 动态列 自动填充数据 -------------------------- + for (int r = 0; r < rowCount; r++) + { + for (int c = 0; c < colCount; c++) + { + ws.Cells[dataStartRow + r, c + 1].Value = dt.Rows[r][c]; + } + } + + // -------------------------- 4. 自动换行 + 行高自适应(关键) -------------------------- + var dataRange = ws.Cells[headerRow, 1, dataStartRow + rowCount - 1, colCount]; + dataRange.Style.Font.Size = 10; + dataRange.Style.WrapText = true; + dataRange.Style.VerticalAlignment = ExcelVerticalAlignment.Center; + dataRange.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; + // 加细边框 + dataRange.Style.Border.Top.Style = ExcelBorderStyle.Thin; + dataRange.Style.Border.Bottom.Style = ExcelBorderStyle.Thin; + dataRange.Style.Border.Left.Style = ExcelBorderStyle.Thin; + dataRange.Style.Border.Right.Style = ExcelBorderStyle.Thin; + + // 自动计算:所有列平均撑满 A4 宽度 + if (listcolwidth != null) + { + int colnum = listcolwidth.Count > colCount ? colCount : listcolwidth.Count; + for (int i = 1; i <= colnum; i++) + { + ws.Column(i).Width = listcolwidth[i - 1]; + } + } + else + { + double totalWidth = 58; + double perColWidth = (double)totalWidth / colCount; + for (int i = 1; i <= colCount; i++) + { + ws.Column(i).Width = perColWidth; + } + } + + // 行高自适应(EPPlus 4.x 兼容) + for (int r = headerRow; r <= headerRow + rowCount - 1; r++) + { + ws.Row(r).CustomHeight = true; + } + + // -------------------------- 5. 表头样式 -------------------------- + var headerRange = ws.Cells[headerRow, 1, headerRow, colCount]; + headerRange.Style.Font.Bold = true; + ws.Row(headerRow).Height = 25; + + // -------------------------- 6. 打印:每页重复标题行 -------------------------- + ws.PrinterSettings.RepeatRows = new ExcelAddress($"{headerRow}:{headerRow}"); + + // -------------------------- 7. 自定义页码(中文样式 + 字体大小) -------------------------- + // &P=当前页 &N=总页 12号字体 居中 + ws.HeaderFooter.OddFooter.CenteredText = "&14 第 &P 页 / 共 &N 页"; + + // -------------------------- 8. 打印格式优化 -------------------------- + ws.PrinterSettings.PaperSize = ePaperSize.A4; + ws.PrinterSettings.Orientation = eOrientation.Portrait; + + ws.PrinterSettings.LeftMargin = 0.4m; + ws.PrinterSettings.RightMargin = 0.4m; + ws.PrinterSettings.TopMargin = 0.8m; + ws.PrinterSettings.BottomMargin = 1.2m; + } + #endregion + } +} diff --git a/src/YhCore.Service/Excel/ExcelRender.cs b/src/YhCore.Service/Excel/ExcelRender.cs new file mode 100644 index 0000000..a592cc7 --- /dev/null +++ b/src/YhCore.Service/Excel/ExcelRender.cs @@ -0,0 +1,404 @@ +using System; +using System.Data; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using NPOI.HSSF.UserModel; +using NPOI.SS.Formula.Eval; +using NPOI.SS.UserModel; +using NPOI.XSSF.UserModel; + +namespace YhCore.Service.Excel +{ + /// + /// 使用 NPOI 操作 Excel,无需 Office COM 组件(移植自旧 Bsch.Common/DocPrint/ExcelRender, + /// 仅保留 6 个 WYSHRole Excel 接口用到的导出/导入成员;控制器用 File() 包装下载)。 + /// + public class ExcelRender + { + static ExcelRender() + { + // ExportDT 列宽按 GB2312 字节长计算:net5 需注册 CodePages 提供程序后才能 GetEncoding(936) + Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); + } + + #region DataTable 导出为 .xls 字节流 + + /// + /// DataTable 导出为 Excel 的 MemoryStream(HSSF .xls;65535 行自动分 sheet;列宽按 GB2312 字节长,上限 50) + /// + static MemoryStream ExportDT(DataTable dtSource, string strHeaderText) + { + HSSFWorkbook workbook = new HSSFWorkbook(); + HSSFSheet sheet = workbook.CreateSheet() as HSSFSheet; + + HSSFCellStyle dateStyle = workbook.CreateCellStyle() as HSSFCellStyle; + HSSFDataFormat format = workbook.CreateDataFormat() as HSSFDataFormat; + dateStyle.DataFormat = format.GetFormat("yyyy-mm-dd"); + + //取得列宽 + int[] arrColWidth = new int[dtSource.Columns.Count]; + foreach (DataColumn item in dtSource.Columns) + { + arrColWidth[item.Ordinal] = Encoding.GetEncoding(936).GetBytes(item.ColumnName.ToString()).Length; + } + for (int i = 0; i < dtSource.Rows.Count; i++) + { + for (int j = 0; j < dtSource.Columns.Count; j++) + { + int intTemp = Encoding.GetEncoding(936).GetBytes(dtSource.Rows[i][j].ToString()).Length; + if (intTemp > arrColWidth[j]) + { + arrColWidth[j] = intTemp; + if (intTemp >= 50) + arrColWidth[j] = 50; + } + } + } + int rowIndex = 0; + + foreach (DataRow row in dtSource.Rows) + { + #region 新建表,填充表头,填充列头,样式 + + if (rowIndex == 65535 || rowIndex == 0) + { + if (rowIndex != 0) + { + sheet = workbook.CreateSheet() as HSSFSheet; + } + + #region 列头及样式 + + { + HSSFRow headerRow = sheet.CreateRow(0) as HSSFRow; + + HSSFCellStyle headStyle = workbook.CreateCellStyle() as HSSFCellStyle; + headStyle.Alignment = NPOI.SS.UserModel.HorizontalAlignment.Center; + HSSFFont font = workbook.CreateFont() as HSSFFont; + font.FontHeightInPoints = 10; + font.Boldweight = 700; + headStyle.SetFont(font); + + foreach (DataColumn column in dtSource.Columns) + { + headerRow.CreateCell(column.Ordinal).SetCellValue(column.ColumnName); + headerRow.GetCell(column.Ordinal).CellStyle = headStyle; + + //设置列宽 + sheet.SetColumnWidth(column.Ordinal, (arrColWidth[column.Ordinal] + 1) * 256); + } + } + + #endregion + + rowIndex = 1; + } + + #endregion + + #region 填充内容 + + HSSFRow dataRow = sheet.CreateRow(rowIndex) as HSSFRow; + foreach (DataColumn column in dtSource.Columns) + { + HSSFCell newCell = dataRow.CreateCell(column.Ordinal) as HSSFCell; + + string drValue = row[column].ToString(); + + switch (column.DataType.ToString()) + { + case "System.String": //字符串类型 + newCell.SetCellValue(drValue); + break; + case "System.DateTime": //日期类型 + DateTime dateV; + DateTime.TryParse(drValue, out dateV); + newCell.SetCellValue(dateV); + newCell.CellStyle = dateStyle; //格式化显示 + break; + case "System.Boolean": //布尔型 + bool boolV = false; + bool.TryParse(drValue, out boolV); + newCell.SetCellValue(boolV); + break; + case "System.Int16": //整型 + case "System.Int32": + case "System.Int64": + case "System.Byte": + int intV = 0; + int.TryParse(drValue, out intV); + newCell.SetCellValue(intV); + break; + case "System.Decimal": //浮点型 + case "System.Double": + double doubV = 0; + double.TryParse(drValue, out doubV); + newCell.SetCellValue(doubV); + break; + case "System.DBNull": //空值处理 + newCell.SetCellValue(""); + break; + default: + newCell.SetCellValue(""); + break; + } + } + + #endregion + + rowIndex++; + } + using (MemoryStream ms = new MemoryStream()) + { + workbook.Write(ms); + ms.Flush(); + ms.Position = 0; + + return ms; + } + } + + /// 导出 Excel 字节流(供 Service 层返回 byte[],控制器包 File 下载) + public static byte[] RenderToExcelBytes(DataTable table, string fileName) + { + try { return ExportDT(table, null).ToArray(); } + catch (Exception) { return Array.Empty(); } + } + + #endregion + + #region 读取 excel 到 DataTable + + /// Excel 文档流是否有数据(默认第一个 sheet) + public static bool HasData(Stream excelFileStream, string fileExt) + { + return HasData(excelFileStream, 0, fileExt); + } + + public static bool HasData(Stream excelFileStream, int sheetIndex, string fileExt) + { + using (excelFileStream) + { + IWorkbook workbook = null; + if (fileExt == ".xls") + { + workbook = new HSSFWorkbook(excelFileStream); + } + else if (fileExt == ".xlsx") + { + workbook = new XSSFWorkbook(excelFileStream); + } + + { + if (workbook.NumberOfSheets > 0) + { + if (sheetIndex < workbook.NumberOfSheets) + { + ISheet sheet = workbook.GetSheetAt(sheetIndex); + { + return sheet.PhysicalNumberOfRows > 0; + } + } + } + } + } + return false; + } + + /// + /// Excel 文档流转换成 DataTable + /// + /// Excel 文档流 + /// 表索引号,如第一个表为 0 + /// 标题行索引号,如第一行为 0 + public static DataTable RenderFromExcel(Stream excelFileStream, int sheetIndex, int headerRowIndex, string fileExt) + { + DataTable table = null; + + using (excelFileStream) + { + IWorkbook workbook = null; + if (fileExt == ".xls") + { + workbook = new HSSFWorkbook(excelFileStream); + } + else if (fileExt == ".xlsx") + { + workbook = new XSSFWorkbook(excelFileStream); + } + + ISheet sheet = workbook.GetSheetAt(sheetIndex); + { + table = ImportDt(sheet, headerRowIndex, true); + } + } + return table; + } + + /// 将指定 sheet 中的数据导出到 datatable 中(第一行为列头) + static DataTable ImportDt(ISheet sheet, int HeaderRowIndex, bool needHeader) + { + DataTable table = new DataTable(); + IRow headerRow; + int cellCount; + try + { + if (HeaderRowIndex < 0 || !needHeader) + { + headerRow = sheet.GetRow(0); + cellCount = headerRow.LastCellNum; + + for (int i = headerRow.FirstCellNum; i <= cellCount; i++) + { + DataColumn column = new DataColumn(Convert.ToString(i)); + table.Columns.Add(column); + } + } + else + { + headerRow = sheet.GetRow(HeaderRowIndex); + cellCount = headerRow.LastCellNum; + + for (int i = headerRow.FirstCellNum; i < cellCount; i++) + { + if (headerRow.GetCell(i) == null) + { + if (table.Columns.IndexOf(Convert.ToString(i)) > 0) + { + DataColumn column = new DataColumn(Convert.ToString("重复列名" + i)); + table.Columns.Add(column); + } + else + { + DataColumn column = new DataColumn(Convert.ToString(i)); + table.Columns.Add(column); + } + } + else if (table.Columns.IndexOf(headerRow.GetCell(i).ToString()) > 0) + { + DataColumn column = new DataColumn(Convert.ToString("重复列名" + i)); + table.Columns.Add(column); + } + else + { + DataColumn column = new DataColumn(headerRow.GetCell(i).ToString()); + table.Columns.Add(column); + } + } + } + int rowCount = sheet.LastRowNum; + for (int i = (HeaderRowIndex + 1); i <= sheet.LastRowNum; i++) + { + try + { + IRow row; + if (sheet.GetRow(i) == null) + { + row = sheet.CreateRow(i); + } + else + { + row = sheet.GetRow(i); + } + + DataRow dataRow = table.NewRow(); + + for (int j = row.FirstCellNum; j < cellCount; j++) + { + try + { + if (row.GetCell(j) != null) + { + switch (row.GetCell(j).CellType) + { + case CellType.String: + string str = row.GetCell(j).StringCellValue; + if (str != null && str.Length > 0) + { + dataRow[j] = str.ToString(); + } + else + { + dataRow[j] = null; + } + break; + case CellType.Numeric: + if (DateUtil.IsCellDateFormatted(row.GetCell(j))) + { + dataRow[j] = DateTime.FromOADate(row.GetCell(j).NumericCellValue); + } + else + { + dataRow[j] = Convert.ToDouble(row.GetCell(j).NumericCellValue); + } + break; + case CellType.Boolean: + dataRow[j] = Convert.ToString(row.GetCell(j).BooleanCellValue); + break; + case CellType.Error: + dataRow[j] = ErrorEval.GetText(row.GetCell(j).ErrorCellValue); + break; + case CellType.Formula: + switch (row.GetCell(j).CachedFormulaResultType) + { + case CellType.String: + string strFORMULA = row.GetCell(j).StringCellValue; + if (strFORMULA != null && strFORMULA.Length > 0) + { + dataRow[j] = strFORMULA.ToString(); + } + else + { + dataRow[j] = null; + } + break; + case CellType.Numeric: + dataRow[j] = Convert.ToString(row.GetCell(j).NumericCellValue); + break; + case CellType.Boolean: + dataRow[j] = Convert.ToString(row.GetCell(j).BooleanCellValue); + break; + case CellType.Error: + dataRow[j] = ErrorEval.GetText(row.GetCell(j).ErrorCellValue); + break; + default: + dataRow[j] = ""; + break; + } + break; + default: + dataRow[j] = ""; + break; + } + } + } + catch (Exception) + { + } + } + table.Rows.Add(dataRow); + } + catch (Exception) + { + } + } + } + catch (Exception) + { + } + return table; + } + + #endregion + + public static byte[] StreamToBytes(Stream stream) + { + byte[] bytes = new byte[stream.Length]; + stream.Read(bytes, 0, bytes.Length); + // 设置当前流的位置为流的开始 + stream.Seek(0, SeekOrigin.Begin); + return bytes; + } + } +} diff --git a/src/YhCore.Service/Pdf/NumberToChineseConverter.cs b/src/YhCore.Service/Pdf/NumberToChineseConverter.cs new file mode 100644 index 0000000..98baac4 --- /dev/null +++ b/src/YhCore.Service/Pdf/NumberToChineseConverter.cs @@ -0,0 +1,123 @@ +using System; + +namespace YhCore.Service.Pdf +{ + /// 数字转中文大写金额(移植自旧 Bsch.Common/DocPrint/NumberToChineseConverter,审批单合计金额用)。 + public class NumberToChineseConverter + { + // 中文大写数字 + private static readonly char[] ChineseNumbers = { '零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖' }; + // 中文大写单位 + private static readonly string[] ChineseUnits = { "分", "角", "元", "拾", "佰", "仟", "万", "拾", "佰", "仟", "亿", "拾", "佰", "仟", "兆" }; + + /// + /// 将数字转换为中文大写金额 + /// + /// 需要转换的数字(支持整数和小数,建议保留两位小数) + /// 中文大写金额字符串 + public static string ToChineseUppercase(decimal number) + { + // 处理负数 + bool isNegative = number < 0; + if (isNegative) + { + number = Math.Abs(number); + } + + // 四舍五入保留两位小数 + number = Math.Round(number, 2); + long integerPart = (long)number; // 整数部分 + int decimalPart = (int)((number - integerPart) * 100); // 小数部分(角分) + + // 处理零值 + if (integerPart == 0 && decimalPart == 0) + { + return "零元整"; + } + + string result = string.Empty; + + // 处理整数部分 + if (integerPart > 0) + { + string integerStr = integerPart.ToString(); + int length = integerStr.Length; + + // 标记是否需要加零 + bool needZero = false; + + for (int i = 0; i < length; i++) + { + int digit = int.Parse(integerStr[i].ToString()); + int unitIndex = length - i - 1 + 2; // 对应到ChineseUnits的索引(从元开始) + + if (digit == 0) + { + needZero = true; + // 处理万、亿等单位,避免漏单位 + if (unitIndex == 6 || unitIndex == 10) + { + result += ChineseUnits[unitIndex]; + needZero = false; + } + } + else + { + if (needZero) + { + result += ChineseNumbers[0]; + needZero = false; + } + result += ChineseNumbers[digit] + ChineseUnits[unitIndex]; + } + } + } + + // 处理小数部分(角分) + if (decimalPart > 0) + { + int jiao = decimalPart / 10; // 角 + int fen = decimalPart % 10; // 分 + + if (jiao > 0) + { + result += ChineseNumbers[jiao] + ChineseUnits[1]; + } + else if (integerPart > 0) // 有整数部分但角为0,需要加零 + { + result += ChineseNumbers[0]; + } + + if (fen > 0) + { + result += ChineseNumbers[fen] + ChineseUnits[0]; + } + } + else + { + // 无小数部分,加"整" + result += "整"; + } + + // 处理负数 + if (isNegative) + { + result = "负" + result; + } + + return result; + } + + // 重载方法,支持double类型 + public static string ToChineseUppercase(double number) + { + return ToChineseUppercase((decimal)number); + } + + // 重载方法,支持int类型 + public static string ToChineseUppercase(int number) + { + return ToChineseUppercase((decimal)number); + } + } +} diff --git a/src/YhCore.Service/Pdf/PdfHelper.cs b/src/YhCore.Service/Pdf/PdfHelper.cs new file mode 100644 index 0000000..f3d2842 --- /dev/null +++ b/src/YhCore.Service/Pdf/PdfHelper.cs @@ -0,0 +1,102 @@ +// PDF helper(移植自旧 Bsch.Common/DocPrint/PdfHelper): +// HTML→PDF 由 wkhtmltopdf 外部进程完成(原 iTextSharp XMLWorker 无 Core 版), +// 多页合并用 iTextSharp.LGPLv2.Core;Base64→PNG 直接落盘。 +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using iTextSharp.text; +using iTextSharp.text.pdf; +using Yh.Platform.Abstractions; +using YhCore.Common.Config; + +namespace YhCore.Service.Pdf +{ + public class PdfHelper + { + private static string WkHtmlToPdfPath => + string.IsNullOrEmpty(Config.WkHtmlToPdfPath) ? "wkhtmltopdf" : Config.WkHtmlToPdfPath; + + // --enable-local-file-access 为 wkhtmltopdf 0.12.6 新增开关(签名图片用本地绝对路径 必需); + // 0.12.5 及以下(如 Debian 10 源)不认识该参数会报 Unknown long argument,按版本探测决定是否追加 + private static readonly Lazy NeedsLocalFileAccessFlag = new Lazy(() => + { + try + { + using var probe = Process.Start(new ProcessStartInfo(WkHtmlToPdfPath, "--version") + { + RedirectStandardOutput = true, + CreateNoWindow = true, + UseShellExecute = false, + }); + if (probe == null) return true; + string output = probe.StandardOutput.ReadToEnd(); + probe.WaitForExit(10000); + var m = Regex.Match(output, @"(\d+)\.(\d+)\.(\d+)"); + if (!m.Success) return true; + var version = new Version(int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value), int.Parse(m.Groups[3].Value)); + return version >= new Version(0, 12, 6); + } + catch + { + return true; + } + }); + + /// 逐页 HTML→PDF 后合并为单个 PDF 字节流(DwonWYSQD 用) + public static byte[] ConvertHtmlTextToPdfByList(List htmlText, string HorV = "V") + => MergePdf(htmlText.Select(HtmlToPdf).ToList()); + + private static byte[] HtmlToPdf(string html) + { + var tmpIn = Path.GetTempFileName() + ".html"; + var tmpOut = Path.GetTempFileName() + ".pdf"; + try + { + File.WriteAllText(tmpIn, html, new UTF8Encoding(false)); + string localFlag = NeedsLocalFileAccessFlag.Value ? " --enable-local-file-access" : ""; + var psi = new ProcessStartInfo(WkHtmlToPdfPath, + $" --encoding utf-8{localFlag} --disable-smart-shrinking \"{tmpIn}\" \"{tmpOut}\"") + { CreateNoWindow = true, UseShellExecute = false }; + using var proc = Process.Start(psi); + if (proc == null || !proc.WaitForExit(60000) || !File.Exists(tmpOut)) + throw new DException("审批单 PDF 生成失败,请检查服务器是否已安装 wkhtmltopdf(或配置 wkhtmltopdf_path 指向其可执行文件)!"); + return File.ReadAllBytes(tmpOut); + } + finally + { + if (File.Exists(tmpIn)) File.Delete(tmpIn); + if (File.Exists(tmpOut)) File.Delete(tmpOut); + } + } + + public static byte[] MergePdf(List pdfBytes) + { + using var ms = new MemoryStream(); + var doc = new Document(); + var copy = new PdfCopy(doc, ms); + doc.Open(); + foreach (var b in pdfBytes) + { + if (b == null || b.Length == 0) continue; + var reader = new PdfReader(b); + for (int i = 1; i <= reader.NumberOfPages; i++) + copy.AddPage(copy.GetImportedPage(reader, i)); + copy.FreeReader(reader); + reader.Close(); + } + doc.Close(); + return ms.ToArray(); + } + + /// 签名 base64 → PNG 落盘(审批单 HTML 内嵌本地路径图片用) + public static void ConvertBase64ToPng(string base64String, string outputFilePath) + { + var bytes = Convert.FromBase64String(base64String); + File.WriteAllBytes(outputFilePath, bytes); + } + } +}