/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "statistics/statistics_xlsx.h" #include "base/zlib_help.h" namespace Statistic::Xlsx { namespace { constexpr auto kEpochSerial = 25569.; constexpr auto kMillisecondsInDay = 86400000.; constexpr auto kStyleGeneral = 0; constexpr auto kStyleDate = 1; constexpr auto kStyleDateTime = 2; constexpr auto kStyleHeader = 3; constexpr auto kStyleHeaderRight = 4; constexpr auto kMaxSheetNameLength = 31; constexpr auto kColumnPadding = 2; constexpr auto kHeaderPadding = 3; constexpr auto kMinColumnWidth = 8; constexpr auto kMaxColumnWidth = 48; constexpr auto kDateWidth = 10; constexpr auto kDateTimeWidth = 16; constexpr auto kValueDigits = 6; constexpr auto kSerialDigits = 10; [[nodiscard]] QString ColumnName(int index) { Expects(index >= 0); auto result = QString(); for (auto i = index + 1; i > 0; i = (i - 1) / 26) { result.prepend(QChar('A' + ((i - 1) % 26))); } return result; } [[nodiscard]] QString Escaped(const QString &text) { auto result = QString(); result.reserve(text.size()); for (const auto &ch : text) { const auto code = ch.unicode(); if (ch == '&') { result.append(u"&"_q); } else if (ch == '<') { result.append(u"<"_q); } else if (ch == '>') { result.append(u">"_q); } else if (ch == '"') { result.append(u"""_q); } else if (ch == '\'') { result.append(u"'"_q); } else if (code >= ' ' || code == '\t' || code == '\n' || code == '\r') { result.append(ch); } } return result; } [[nodiscard]] QString NumberText(float64 value, int digits) { const auto rounded = base::SafeRound(value); return ((value == rounded) && (std::abs(value) < 1e15)) ? QString::number(int64(rounded)) : QString::number(value, 'f', digits); } [[nodiscard]] QString SheetName( const QString &name, int index, int limit = kMaxSheetNameLength) { const auto forbidden = u":\\/?*[]"_q; auto result = QString(); for (const auto &ch : name) { if (!forbidden.contains(ch)) { result.append(ch); } } result = result.trimmed(); if (result.size() > limit) { result = result.mid(0, limit).trimmed(); } return result.isEmpty() ? u"Sheet%1"_q.arg(index + 1) : result; } [[nodiscard]] std::vector UniqueSheetNames( const std::vector &sheets) { auto result = std::vector(); result.reserve(sheets.size()); for (auto i = 0; i != int(sheets.size()); ++i) { auto name = SheetName(sheets[i].name, i); for (auto attempt = 2; ranges::contains(result, name); ++attempt) { const auto suffix = u" (%1)"_q.arg(attempt); name = SheetName( sheets[i].name, i, kMaxSheetNameLength - suffix.size()) + suffix; } result.push_back(name); } return result; } [[nodiscard]] std::vector NumericColumns(const Sheet &sheet) { auto result = std::vector(); auto filled = std::vector(); for (auto i = 1; i != int(sheet.rows.size()); ++i) { const auto &row = sheet.rows[i]; if (result.size() < row.size()) { result.resize(row.size(), true); filled.resize(row.size(), false); } for (auto j = 0; j != int(row.size()); ++j) { switch (row[j].type) { case Cell::Type::Empty: break; case Cell::Type::Number: case Cell::Type::Date: case Cell::Type::DateTime: filled[j] = true; break; default: result[j] = false; break; } } } for (auto i = 0; i != int(result.size()); ++i) { result[i] = result[i] && filled[i]; } return result; } [[nodiscard]] QByteArray CellContent( const Cell &cell, const QString &ref, bool numericColumn) { const auto open = [&](int style, const QString &type) { auto result = u"'); }; const auto text = [&](int style) { return open(style, u"inlineStr"_q) + u""_q + Escaped(cell.text) + u""_q; }; const auto number = [&](int style, int digits) { return open(style, QString()) + u""_q + NumberText(cell.number, digits) + u""_q; }; switch (cell.type) { case Cell::Type::Text: return text(kStyleGeneral).toUtf8(); case Cell::Type::Header: return text(numericColumn ? kStyleHeaderRight : kStyleHeader).toUtf8(); case Cell::Type::Number: return number(kStyleGeneral, kValueDigits).toUtf8(); case Cell::Type::Date: return number(kStyleDate, kSerialDigits).toUtf8(); case Cell::Type::DateTime: return number(kStyleDateTime, kSerialDigits).toUtf8(); } return QByteArray(); } [[nodiscard]] int CellWidth(const Cell &cell) { switch (cell.type) { case Cell::Type::Empty: return 0; case Cell::Type::Text: return cell.text.size() + kColumnPadding; case Cell::Type::Header: return cell.text.size() + kHeaderPadding; case Cell::Type::Number: return NumberText(cell.number, kValueDigits).size() + kColumnPadding; case Cell::Type::Date: return kDateWidth + kColumnPadding; case Cell::Type::DateTime: return kDateTimeWidth + kColumnPadding; } return 0; } [[nodiscard]] QByteArray ColumnsContent(const Sheet &sheet) { auto widths = std::vector(); for (const auto &row : sheet.rows) { if (widths.size() < row.size()) { widths.resize(row.size(), 0); } for (auto i = 0; i != int(row.size()); ++i) { widths[i] = std::max(widths[i], CellWidth(row[i])); } } if (widths.empty()) { return QByteArray(); } auto result = QByteArray(""); for (auto i = 0; i != int(widths.size()); ++i) { const auto width = std::clamp( widths[i], kMinColumnWidth, kMaxColumnWidth); result.append(u""_q.arg(i + 1).arg(width).toUtf8()); } return result.append(""); } [[nodiscard]] bool StartsWithHeader(const Sheet &sheet) { return !sheet.rows.empty() && ranges::any_of(sheet.rows.front(), [](const Cell &cell) { return cell.type == Cell::Type::Header; }); } [[nodiscard]] QByteArray SheetContent(const Sheet &sheet) { const auto numeric = NumericColumns(sheet); auto result = QByteArray(); result.append("" ""); if (StartsWithHeader(sheet)) { result.append("" "" ""); } result.append(ColumnsContent(sheet)); result.append(""); for (auto i = 0; i != int(sheet.rows.size()); ++i) { const auto &row = sheet.rows[i]; const auto number = i + 1; result.append(u""_q.arg(number).toUtf8()); for (auto j = 0; j != int(row.size()); ++j) { if (row[j].type != Cell::Type::Empty) { result.append(CellContent( row[j], ColumnName(j) + QString::number(number), (j < int(numeric.size())) && numeric[j])); } } result.append(""); } result.append(""); return result; } [[nodiscard]] QByteArray StylesContent() { return "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } [[nodiscard]] QByteArray WorkbookContent(const std::vector &names) { auto result = QByteArray(); result.append("" ""); for (auto i = 0; i != int(names.size()); ++i) { result.append(u""_q .arg(Escaped(names[i])) .arg(i + 1) .toUtf8()); } result.append(""); return result; } [[nodiscard]] QByteArray WorkbookRelationsContent(int count) { auto result = QByteArray(); result.append("" ""); const auto add = [&](int id, const QString &type, const QString &target) { result.append(u""_q .arg(id) .arg(type) .arg(target) .toUtf8()); }; for (auto i = 0; i != count; ++i) { add(i + 1, u"worksheet"_q, u"worksheets/sheet%1.xml"_q.arg(i + 1)); } add(count + 1, u"styles"_q, u"styles.xml"_q); result.append(""); return result; } [[nodiscard]] QByteArray RootRelationsContent() { return "" "" "" ""; } [[nodiscard]] QByteArray ContentTypesContent(int count) { auto result = QByteArray(); result.append("" "" "" "" "" ""); for (auto i = 0; i != count; ++i) { result.append(u""_q.arg(i + 1).toUtf8()); } result.append(""); return result; } } // namespace Cell Text(const QString &text) { return { .type = Cell::Type::Text, .text = text }; } Cell Header(const QString &text) { return { .type = Cell::Type::Header, .text = text }; } Cell Number(float64 value) { return { .type = Cell::Type::Number, .number = value }; } Cell Date(float64 milliseconds) { return { .type = Cell::Type::Date, .number = (milliseconds / kMillisecondsInDay) + kEpochSerial, }; } Cell DateTime(float64 milliseconds) { return { .type = Cell::Type::DateTime, .number = (milliseconds / kMillisecondsInDay) + kEpochSerial, }; } QByteArray Serialize(const std::vector &sheets) { if (sheets.empty()) { return QByteArray(); } const auto names = UniqueSheetNames(sheets); const auto count = int(sheets.size()); auto zip = zlib::FileToWrite(); auto info = zip_fileinfo{ { 0, 0, 0, 0, 0, 0 }, 0, 0, 0 }; const auto add = [&](const QString &path, const QByteArray &content) { zip.openNewFile( path.toUtf8().constData(), &info, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, Z_DEFAULT_COMPRESSION); zip.writeInFile(content.constData(), content.size()); zip.closeFile(); }; add(u"[Content_Types].xml"_q, ContentTypesContent(count)); add(u"_rels/.rels"_q, RootRelationsContent()); add(u"xl/workbook.xml"_q, WorkbookContent(names)); add(u"xl/_rels/workbook.xml.rels"_q, WorkbookRelationsContent(count)); add(u"xl/styles.xml"_q, StylesContent()); for (auto i = 0; i != count; ++i) { add( u"xl/worksheets/sheet%1.xml"_q.arg(i + 1), SheetContent(sheets[i])); } zip.close(); return (zip.error() == ZIP_OK) ? zip.result() : QByteArray(); } } // namespace Statistic::Xlsx