import Foundation
import CZlib
import NIOCore
/// Create a simple excel sheet with exactly one sheet and the bare minimum to make it work in office applications
/// Please note that XLSX only support up to 16k columns
public final class XlsxWriter {
let sheet_xml: GzipStream
static var workbook_xml: ByteBuffer {
let workbook_xml = try! GzipStream(level: 6, chunkCapacity: 512)
workbook_xml.write("""
""")
return workbook_xml.finish()
}
static var workbook_xml_rels: ByteBuffer {
let workbook_xml_rels = try! GzipStream(level: 6, chunkCapacity: 512)
workbook_xml_rels.write("""
""")
return workbook_xml_rels.finish()
}
static var rels: ByteBuffer {
let rels = try! GzipStream(level: 6, chunkCapacity: 512)
rels.write("""
""")
return rels.finish()
}
static var content_type: ByteBuffer {
let content_type = try! GzipStream(level: 6, chunkCapacity: 512)
content_type.write( """
""")
return content_type.finish()
}
static var styles_xml: ByteBuffer {
let styles_xml = try! GzipStream(level: 6, chunkCapacity: 512)
styles_xml.write("""
""")
return styles_xml.finish()
}
public init() throws {
sheet_xml = try GzipStream(level: 6, chunkCapacity: 4096)
sheet_xml.write( """
""")
}
public func startRow() {
sheet_xml.write("")
}
public func endRow() {
sheet_xml.write("
")
}
public func write(_ int: Int) {
sheet_xml.write("\(int)")
}
/// Write unix timestamp with iso8601 formated date
public func writeTimestamp(_ timestamp: Timestamp) {
let excelTime = Double(timestamp.timeIntervalSince1970) / 86400 + (70 * 365 + 19)
sheet_xml.write("\(excelTime)")
}
/// Write Float
/// See https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.cell
public func write(_ float: Float, significantDigits: Int) {
if float.isInfinite || float.isNaN {
sheet_xml.write("#NUM!")
} else {
sheet_xml.write("\(String(format: "%.\(significantDigits)f", float))")
}
}
/// Write string
public func write(_ string: String) {
sheet_xml.write("\(string)")
}
func write(timestamp: Timestamp = .now()) -> ByteBuffer {
sheet_xml.write("")
return ZipWriter.zip(files: [
(path: "[Content_Types].xml", compressed: Self.content_type),
(path: "xl/workbook.xml", compressed: Self.workbook_xml),
(path: "xl/_rels/workbook.xml.rels", compressed: Self.workbook_xml_rels),
(path: "_rels/.rels", compressed: Self.rels),
(path: "xl/styles.xml", compressed: Self.styles_xml),
(path: "xl/worksheets/sheet1.xml", compressed: sheet_xml.finish())
], timestamp: timestamp)
}
}
enum ZipStreamError: Error {
case zlibDeflateInitInvalidParameter
case zlibInsufficientMemory
case zlibVersionError
case deflateInitFailed(code: Int32)
}
/// Gzip Stream compressor w
public final class GzipStream {
var zstream: UnsafeMutablePointer
var writebuffer: ByteBuffer
public init(level: Int32 = 6, chunkCapacity: Int = 4096) throws {
zstream = UnsafeMutablePointer.allocate(capacity: 1)
zstream.pointee.zalloc = nil
zstream.pointee.zfree = nil
let ret = deflateInit2_(zstream, level, Z_DEFLATED, 15 | 16, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY, ZLIB_VERSION, Int32(MemoryLayout.size))
guard ret != Z_STREAM_ERROR else {
throw ZipStreamError.zlibDeflateInitInvalidParameter
}
guard ret != Z_MEM_ERROR else {
throw ZipStreamError.zlibInsufficientMemory
}
guard ret != Z_VERSION_ERROR else {
throw ZipStreamError.zlibVersionError
}
guard ret == Z_OK else {
throw ZipStreamError.deflateInitFailed(code: ret)
}
self.writebuffer = ByteBufferAllocator().buffer(capacity: chunkCapacity)
writebuffer.withUnsafeMutableWritableBytes { ptr in
zstream.pointee.avail_out = UInt32(ptr.count)
zstream.pointee.next_out = ptr.baseAddress?.assumingMemoryBound(to: Bytef.self)
zstream.pointee.total_out = 0
}
}
public func write(_ str: String) {
str.withContiguousStorageIfAvailable { body -> Void in
compress(data: UnsafeRawBufferPointer(body), flush: Z_NO_FLUSH)
} ?? {
var str = str
str.withUTF8({ body in
compress(data: UnsafeRawBufferPointer(body), flush: Z_NO_FLUSH)
})
}()
}
/// flush and return data
public func finish() -> ByteBuffer {
compress(data: nil, flush: Z_FINISH)
return writebuffer
}
/// compress
func compress(data: UnsafeRawBufferPointer?, flush: Int32) {
if let data = data {
// set input
zstream.pointee.next_in = UnsafeMutablePointer(mutating: data.bindMemory(to: UInt8.self).baseAddress)
zstream.pointee.avail_in = UInt32(data.count)
}
repeat {
if zstream.pointee.avail_out == 0 {
/// Increase buffer capacity. Always double underlaying storage.
writebuffer.reserveCapacity(minimumWritableBytes: writebuffer.writerIndex)
writebuffer.withUnsafeMutableWritableBytes({ ptr in
zstream.pointee.avail_out = uInt(ptr.count)
zstream.pointee.next_out = ptr.baseAddress?.assumingMemoryBound(to: Bytef.self)
})
}
let avail_out_before = zstream.pointee.avail_out
let ret = deflate(zstream, flush)
writebuffer.moveWriterIndex(forwardBy: Int(avail_out_before - zstream.pointee.avail_out))
if ret == Z_STREAM_END {
break
}
guard ret == Z_OK else {
fatalError("deflate loop error, \(ret)")
}
} while zstream.pointee.avail_out == 0
}
deinit {
deflateEnd(zstream)
zstream.deallocate()
}
}
fileprivate extension Data {
mutating func append(_ value: UInt32) {
Swift.withUnsafeBytes(of: value) {
self.append(contentsOf: $0)
}
}
mutating func append(_ value: UInt16) {
Swift.withUnsafeBytes(of: value) {
self.append(contentsOf: $0)
}
}
}
public struct ZipWriter {
/// compressed input data must be gzip compressed with correct gzip headers
public static func zip(files: [(path: String, compressed: ByteBuffer)], timestamp: Timestamp = .now()) -> ByteBuffer {
let totalSize = files.reduce(22, {
$0 + $1.path.count * 2 + $1.compressed.writerIndex - 18 + 30 + 46
})
var out = ByteBufferAllocator().buffer(capacity: totalSize)
let date = timestamp.toComponents()
let modificationDate = UInt16(date.day) | ((UInt16(date.month) << 5)) | ((UInt16(date.year - 1980) << 9))
let modificationTime = UInt16(timestamp.second) | ((UInt16(timestamp.minute) << 5)) | ((UInt16(timestamp.hour) << 11))
// print local file header and compressed data
var localHeaderOffsets = [Int]()
localHeaderOffsets.reserveCapacity(files.count)
for (path, compressed) in files {
localHeaderOffsets.append(out.writerIndex)
out.writeInteger(UInt32(0x04034b50), endianness: .little) // local fileheader signature
out.writeInteger(UInt16(0x0014), endianness: .little) // version
out.writeInteger(UInt16(0x0000), endianness: .little) // bitflag
out.writeData([(compressed.getInteger(at: 2) as UInt8?)!, 0]) // compression method, lzma
out.writeInteger(modificationTime, endianness: .little)
out.writeInteger(modificationDate, endianness: .little)
out.writeInteger((compressed.getInteger(at: compressed.writerIndex-8) as UInt32?)!) // crc
out.writeInteger(UInt32(compressed.writerIndex - 10 - 8), endianness: .little) // compressed size
out.writeInteger((compressed.getInteger(at: compressed.writerIndex-4) as UInt32?)!) // uncompressed size
out.writeInteger(UInt16(path.count), endianness: .little) // filename length
out.writeInteger(UInt16(0x0000), endianness: .little) // extra field length
out.writeString(path) // filename
var payload = compressed.getSlice(at: 10, length: compressed.writerIndex-8-10)!
out.writeBuffer(&payload) // compressed payload without header
}
let centralDirOffset = out.writerIndex
// print central directory header
for (i, (path, compressed)) in files.enumerated() {
out.writeInteger(UInt32(0x02014b50), endianness: .little) // signature
out.writeInteger(UInt16(0x0000), endianness: .little) // version generated by
out.writeInteger(UInt16(0x0014), endianness: .little) // version needed
out.writeInteger(UInt16(0x0000), endianness: .little) // bit flag
out.writeData([(compressed.getInteger(at: 2) as UInt8?)!, 0]) // compression method, lzma
out.writeInteger(modificationTime, endianness: .little)
out.writeInteger(modificationDate, endianness: .little)
out.writeInteger((compressed.getInteger(at: compressed.writerIndex-8) as UInt32?)!) // crc
out.writeInteger(UInt32(compressed.writerIndex - 10 - 8), endianness: .little) // compressed size
out.writeInteger((compressed.getInteger(at: compressed.writerIndex-4) as UInt32?)!) // uncompressed size
out.writeInteger(UInt16(path.count), endianness: .little) // filename length
out.writeInteger(UInt16(0x0000), endianness: .little) // extra field length
out.writeInteger(UInt16(0x0000), endianness: .little) // comment length
out.writeInteger(UInt16(0x0000), endianness: .little) // disk number start
out.writeInteger(UInt16(0x0000), endianness: .little) // internal attributes
out.writeInteger(UInt32(0x0000), endianness: .little) // external attributes
out.writeInteger(UInt32(localHeaderOffsets[i]), endianness: .little)
out.writeString(path) // filename
}
let centralDirSize = out.writerIndex - centralDirOffset
// end central directory
out.writeInteger(UInt32(0x06054b50), endianness: .little) // sig
out.writeInteger(UInt16(0x0000), endianness: .little) // number of disks
out.writeInteger(UInt16(0x0000), endianness: .little) // number of disks start
out.writeInteger(UInt16(files.count), endianness: .little) // number disk entries
out.writeInteger(UInt16(files.count), endianness: .little) // number central directory entries
out.writeInteger(UInt32(centralDirSize), endianness: .little)
out.writeInteger(UInt32(centralDirOffset), endianness: .little)
out.writeInteger(UInt16(0x00), endianness: .little) // zip comment length
precondition(totalSize == out.writerIndex)
return out
}
}