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 } }