All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.beangle.doc.excel.html.TableWriter.scala Maven / Gradle / Ivy

/*
 * Copyright (C) 2005, The Beangle Software.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see .
 */

package org.beangle.doc.excel.html

import org.apache.poi.ss.usermodel.{Cell, HorizontalAlignment, Sheet, VerticalAlignment}
import org.apache.poi.ss.util.{CellRangeAddress, RegionUtil}
import org.apache.poi.xssf.usermodel.*
import org.beangle.commons.collection.Collections
import org.beangle.commons.lang.Strings
import org.beangle.doc.html.HtmlParser
import org.beangle.doc.html.dom.*

object TableWriter {
  def writer(html: String): XSSFWorkbook = {
    val workbook = new XSSFWorkbook()
    val body = HtmlParser.parse(html).body

    val tables = body.childNodes.filter(_.name == "table")
    tables foreach { table =>
      val sheetName = table.attributes.get("data-sheet-name").orNull
      var sheet: XSSFSheet = null
      if (null == sheetName) {
        sheet = workbook.createSheet()
      } else {
        sheet = workbook.createSheet(sheetName)
      }
      TableWriter.write(table.asInstanceOf[Table], sheet, 0)
    }
    workbook
  }

  def write(table: Table, sheet: XSSFSheet, startRowIdx: Int): Unit = {
    val writer = new TableWriter(table, sheet)
    writer.write(startRowIdx)
  }
}

class TableWriter(table: Table, sheet: XSSFSheet) {
  private var rowIdx = -1
  private var colIndex = -1
  private val merged = Collections.newSet[(Int, Int)]
  private val styles = Collections.newMap[String, XSSFCellStyle]
  private val fonts = Collections.newMap[String, XSSFFont]
  private val defaultStyle = buildDefaultStyle(sheet.getWorkbook)
  private var widths: Array[Length] = _

  private def buildDefaultStyle(wb: XSSFWorkbook): XSSFCellStyle = {
    val style = wb.createCellStyle
    style.setAlignment(HorizontalAlignment.CENTER)
    style.setVerticalAlignment(VerticalAlignment.CENTER)
    style.setWrapText(true)
    style
  }

  def write(startRowIdx: Int): Unit = {
    rowIdx = startRowIdx - 1
    widths = table.widths
    table.caption foreach { caption =>
      writeRow(sheet, caption.text.get, rowIdx, widths.length)
    }
    widths.indices foreach { idx =>
      val width = widths(idx)
      if (null != width) sheet.setColumnWidth(idx, (width.charNums * 256).intValue)
    }

    table.thead.foreach { thead =>
      thead.rows foreach { tr =>
        val row = createRow()
        colIndex = -1
        tr.style.height foreach { height =>
          row.setHeightInPoints(height.points.floatValue)
        }
        tr.cells foreach { td =>
          val cell = createCell(row)
          fillin(cell, td)
          createMergeRegin(rowIdx, colIndex, td)
        }
      }
    }
    sheet.createFreezePane(0, rowIdx + 1) //冻结所在行及其之前的行

    table.tbodies foreach { tbody =>
      tbody.rows foreach { tr =>
        val row = createRow()
        colIndex = -1
        tr.style.height foreach { height =>
          row.setHeightInPoints(height.points.floatValue)
        }
        tr.cells foreach { td =>
          val cell = createCell(row)
          fillin(cell, td)
          createMergeRegin(rowIdx, colIndex, td)
        }
      }
    }
    //set style of merged regions
    val regionsIter = sheet.getMergedRegions.iterator()
    while (regionsIter.hasNext) {
      val region = regionsIter.next()
      val cell = sheet.getRow(region.getFirstRow).getCell(region.getFirstColumn)
      val cs = cell.getCellStyle
      RegionUtil.setBorderTop(cs.getBorderTop, region, sheet)
      RegionUtil.setTopBorderColor(cs.getTopBorderColor, region, sheet)
      RegionUtil.setBorderRight(cs.getBorderRight, region, sheet)
      RegionUtil.setRightBorderColor(cs.getRightBorderColor, region, sheet)
      RegionUtil.setBorderBottom(cs.getBorderBottom, region, sheet)
      RegionUtil.setBottomBorderColor(cs.getBottomBorderColor, region, sheet)
      RegionUtil.setBorderLeft(cs.getBorderLeft, region, sheet)
      RegionUtil.setLeftBorderColor(cs.getLeftBorderColor, region, sheet)
    }
    //view or print
    table.attributes.get("data-repeating-rows") foreach { repeatRows =>
      sheet.setRepeatingRows(CellRangeAddress.valueOf(repeatRows))
    }
    table.attributes.get("data-zoom") foreach { zoom =>
      sheet.setZoom(zoom.toInt)
    }
    table.attributes.get("data-print-scale") foreach { scale =>
      sheet.getPrintSetup.setScale(scale.toShort)
    }
  }

  private def createRow(): XSSFRow = {
    rowIdx += 1
    sheet.createRow(rowIdx)
  }

  private def fillin(cell: Cell, td: Table.Cell): Unit = {
    val style = td.style
    td.text foreach { texts =>
      //设置行高
      style.height match
        case None =>
          if (td.rowspan == 1) {
            val lines = Strings.count(texts, '\n') + 1
            if (lines > 1) {
              val newHeight = lines * sheet.getDefaultRowHeightInPoints
              if (newHeight > cell.getRow.getHeightInPoints) {
                cell.getRow.setHeightInPoints(newHeight)
              }
            }
          }
        case Some(height) =>
          cell.getRow.setHeightInPoints(height.points.floatValue)

      //填充富文本字符串
      style.font.flatMap(_.asciiFont) match
        case None => cell.setCellValue(texts)
        case Some(asciiFont) =>
          val parts = splitText(texts, asciiFont, style.font.get)
          if (td.rowspan == 1 && style.height.isEmpty) { //不跨行,且没有指定高度的情况下
            val newLines = Math.ceil(texts.length * 1.0 / getCharNums(td))
            val newHeight = newLines * sheet.getDefaultRowHeightInPoints
            if (newHeight > cell.getRow.getHeightInPoints) {
              cell.getRow.setHeightInPoints(newHeight.floatValue)
            }
          }
          val str = new XSSFRichTextString(parts.map(_.value).mkString)
          var pos = 0
          parts foreach { part =>
            part.font foreach { f => str.applyFont(pos, pos + part.value.length, getOrCreateFont(f)) }
            pos += part.value.length
          }
          cell.setCellValue(str)

      // 设置样式和文字方向
      cell.setCellStyle(getOrCreateStyle(td, style))
    }
  }

  private def getCharNums(td: Table.Cell): Int = {
    var charNums = 0d
    (0 until td.colspan) foreach { i =>
      if (widths(colIndex + i) != null) {
        charNums += widths(colIndex + i).charNums
      }
    }
    charNums.toInt
  }

  /** 拆分成西文和汉字不同的字体
   * @param text
   * @param asciiFont
   * @param defaultFont
   * @return
   */
  private def splitText(text: String, asciiFont: Font, defaultFont: Font): Seq[FontText] = {
    if (Strings.isBlank(text)) {
      List.empty
    } else {
      val chars = text.toCharArray
      val parts = Collections.newBuffer[FontText]
      val buf = new StringBuilder()
      var isIdeographic = Character.isIdeographic(chars(0))
      chars foreach { c =>
        val nextIsIdeographic = Character.isIdeographic(c)
        if nextIsIdeographic == isIdeographic then
          buf.append(c)
        else
          parts.append(FontText(buf.toString(), Some(if isIdeographic then defaultFont else asciiFont)))
          buf.clear()
          buf.append(c)
          isIdeographic = nextIsIdeographic
      }
      if (buf.nonEmpty) parts.append(FontText(buf.toString(), Some(if isIdeographic then defaultFont else asciiFont)))
      parts.toSeq
    }
  }

  private def getOrCreateStyle(td: Table.Cell, style: Style): XSSFCellStyle = {
    styles.get(style.toString) match
      case None =>
        if (style.properties.isEmpty) {
          defaultStyle
        } else {
          val wb = sheet.getWorkbook
          val s = wb.createCellStyle
          val align = style.textAlign.getOrElse("left") match {
            case "center" => HorizontalAlignment.CENTER
            case "right" => HorizontalAlignment.RIGHT
            case _ => HorizontalAlignment.LEFT
          }
          s.setAlignment(align)

          val valign = style.verticalAlign.getOrElse("middle") match {
            case "top" => VerticalAlignment.TOP
            case "bottom" => VerticalAlignment.BOTTOM
            case _ => VerticalAlignment.CENTER
          }

          s.setVerticalAlignment(valign)
          s.setWrapText(true)
          style.border foreach { b =>
            Styles.convertBorder(b.top) foreach { d =>
              s.setBorderTop(d._1)
              s.setTopBorderColor(d._2)
            }
            Styles.convertBorder(b.right) foreach { d =>
              s.setBorderRight(d._1)
              s.setRightBorderColor(d._2)
            }
            Styles.convertBorder(b.bottom) foreach { d =>
              s.setBorderBottom(d._1)
              s.setBottomBorderColor(d._2)
            }
            Styles.convertBorder(b.left) foreach { d =>
              s.setBorderLeft(d._1)
              s.setLeftBorderColor(d._2)
            }
          }
          style.font foreach { f => s.setFont(getOrCreateFont(f)) }
          if (style.has("writing-mode", "vertical-rl") && style.has("text-orientation")) {
            s.setRotation(255)
          }
          styles.put(style.toString, s)
          s
        }
      case Some(st) => st
  }

  private def getOrCreateFont(f: Font): XSSFFont = {
    fonts.get(f.toString) match
      case None =>
        val font = sheet.getWorkbook.createFont
        f.family foreach { fm => font.setFontName(fm) }
        f.bold foreach { fm => font.setBold(true) }
        f.size foreach { s => font.setFontHeightInPoints(s) }
        f.strikeout foreach { s => font.setStrikeout(s) }
        Styles.convertUnderLine(f.underline) foreach { s => font.setUnderline(s) }
        fonts.put(f.toString, font)
        font
      case Some(font) => font
  }

  private def createCell(row: XSSFRow): XSSFCell = {
    colIndex += 1
    while (merged.contains((rowIdx, colIndex))) {
      colIndex += 1
    }
    row.createCell(colIndex)
  }

  private def createMergeRegin(rowIdx: Int, colIdx: Int, cell: Table.Cell): Unit = {
    if (cell.colspan > 1 || cell.rowspan > 1) {
      val mergedRegion = new CellRangeAddress(rowIdx, rowIdx + cell.rowspan - 1, colIdx, colIdx + cell.colspan - 1)
      sheet.addMergedRegion(mergedRegion)
    }
    if (cell.rowspan > 1) {
      for (r <- 1 until cell.rowspan; c <- colIndex until colIndex + cell.colspan) {
        merged.add(r + rowIdx, c)
      }
    }
    colIndex += cell.colspan - 1 //colIndex 最后一个写的位置,不是空位
  }

  private def writeRow(sheet: Sheet, content: String, rowIdx: Int, colSpan: Int): Cell = {
    val mergedRegion = new CellRangeAddress(rowIdx, rowIdx, 0, colSpan - 1)
    sheet.addMergedRegion(mergedRegion)

    val row = createRow()
    val cell = row.createCell(0)

    val newLines = Strings.count(content.trim(), "\n")
    if (newLines > 0) {
      row.setHeightInPoints((newLines + 1) * sheet.getDefaultRowHeightInPoints)
    }
    cell.setCellValue(content)
    cell
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy