From efbda191ab5f0d2d2158a0be89fa7b6294097cb4 Mon Sep 17 00:00:00 2001 From: gre-ilya Date: Wed, 29 Apr 2026 10:48:45 +0500 Subject: [PATCH] changed lib --- .gitignore | 3 + DocxBuilder.cpp | 621 +++++++++++++++++++++--------------------------- DocxWizard.pro | 24 +- 3 files changed, 279 insertions(+), 369 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f47441e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +*.swp + diff --git a/DocxBuilder.cpp b/DocxBuilder.cpp index 5f70d0d..92f5a94 100644 --- a/DocxBuilder.cpp +++ b/DocxBuilder.cpp @@ -2,358 +2,310 @@ #include #include -#include -#include #include -#include +#include #include -// Qt ZIP support (входит в состав Qt5 через QtCore private или quazip). -// Здесь используем простую реализацию через QZipWriter (Qt >= 5.x, модуль QtCore/private). -// Если QZipWriter недоступен — подключите QuaZip: https://github.com/stachenov/quazip -#include -#include +// zlib входит в состав Qt и доступна на любой платформе без дополнительных зависимостей +#include + +// ============================================================ +// Минимальный ZIP-writer без внешних библиотек +// (только zlib, которая есть везде) +// ============================================================ +namespace { + +quint32 crc32_buf(const QByteArray& data) { + return static_cast( + ::crc32(0, reinterpret_cast(data.constData()), + static_cast(data.size()))); +} + +QByteArray deflateRaw(const QByteArray& input) { + z_stream zs{}; + deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + -15, 8, Z_DEFAULT_STRATEGY); + + QByteArray out; + out.resize(static_cast(deflateBound(&zs, static_cast(input.size())))); + zs.next_in = reinterpret_cast(const_cast(input.constData())); + zs.avail_in = static_cast(input.size()); + zs.next_out = reinterpret_cast(out.data()); + zs.avail_out = static_cast(out.size()); + deflate(&zs, Z_FINISH); + out.resize(static_cast(zs.total_out)); + deflateEnd(&zs); + return out; +} + +void u16(QByteArray& b, quint16 v) { + b += char(v & 0xFF); b += char(v >> 8); +} +void u32(QByteArray& b, quint32 v) { + b += char(v & 0xFF); b += char((v>>8)&0xFF); + b += char((v>>16)&0xFF); b += char((v>>24)&0xFF); +} + +struct ZEntry { QString name; QByteArray comp, orig; quint32 crc, off; quint16 meth; }; + +class ZipWriter { +public: + bool open(const QString& p) { + f.setFileName(p); + return f.open(QIODevice::WriteOnly | QIODevice::Truncate); + } + void add(const QString& name, const QByteArray& data) { + ZEntry e; + e.name = name; e.orig = data; + e.crc = crc32_buf(data); + e.off = static_cast(f.pos()); + QByteArray c = deflateRaw(data); + if (c.size() < data.size()) { e.comp = c; e.meth = 8; } + else { e.comp = data; e.meth = 0; } + + QByteArray nb = name.toUtf8(); + QByteArray lh; + u32(lh,0x04034b50); u16(lh,20); u16(lh,0x0800); u16(lh,e.meth); + u16(lh,0); u16(lh,0); + u32(lh,e.crc); + u32(lh,static_cast(e.comp.size())); + u32(lh,static_cast(e.orig.size())); + u16(lh,static_cast(nb.size())); u16(lh,0); + lh += nb; + f.write(lh); f.write(e.comp); + entries << e; + } + void close() { + quint32 cdOff = static_cast(f.pos()); + for (auto& e : entries) { + QByteArray nb = e.name.toUtf8(), cd; + u32(cd,0x02014b50); u16(cd,20); u16(cd,20); u16(cd,0x0800); + u16(cd,e.meth); u16(cd,0); u16(cd,0); + u32(cd,e.crc); + u32(cd,static_cast(e.comp.size())); + u32(cd,static_cast(e.orig.size())); + u16(cd,static_cast(nb.size())); + u16(cd,0); u16(cd,0); u16(cd,0); u16(cd,0); u32(cd,0); u32(cd,e.off); + cd += nb; f.write(cd); + } + quint32 cdSz = static_cast(f.pos()) - cdOff; + QByteArray eocd; + u32(eocd,0x06054b50); u16(eocd,0); u16(eocd,0); + u16(eocd,static_cast(entries.size())); + u16(eocd,static_cast(entries.size())); + u32(eocd,cdSz); u32(eocd,cdOff); u16(eocd,0); + f.write(eocd); f.close(); + } +private: + QFile f; QList entries; +}; + +} // namespace // ============================================================ DocxBuilder::DocxBuilder() = default; -void DocxBuilder::clear() { - m_elements.clear(); - m_images.clear(); - m_imageCounter = 0; -} - -// ============================================================ -// Добавление элементов -// ============================================================ +void DocxBuilder::clear() { m_elements.clear(); m_images.clear(); m_imageCounter = 0; } void DocxBuilder::addHeading(const QString& text, int level) { DocElement el; el.text = text; - switch (level) { - case 1: el.type = DocElementType::Heading1; break; - case 2: el.type = DocElementType::Heading2; break; - default: el.type = DocElementType::Heading3; break; - } + el.type = (level==1) ? DocElementType::Heading1 + : (level==2) ? DocElementType::Heading2 + : DocElementType::Heading3; el.style.font = "Times New Roman"; el.style.bold = true; - el.style.fontSize = (level == 1) ? 36 : (level == 2) ? 30 : 26; + el.style.fontSize = (level==1) ? 36 : (level==2) ? 30 : 26; m_elements.append(el); } void DocxBuilder::addParagraph(const QString& text, const DocStyle& style) { - DocElement el; - el.type = DocElementType::NormalText; - el.text = text; - el.style = style; + DocElement el; el.type = DocElementType::NormalText; el.text = text; el.style = style; m_elements.append(el); } void DocxBuilder::addTable(const TableData& table) { - DocElement el; - el.type = DocElementType::Table; - el.table = table; + DocElement el; el.type = DocElementType::Table; el.table = table; m_elements.append(el); } void DocxBuilder::addImage(const QString& imagePath, int widthEmu, int heightEmu) { QFileInfo fi(imagePath); - if (!fi.exists()) { - qWarning() << "DocxBuilder::addImage — файл не найден:" << imagePath; - return; - } + if (!fi.exists()) { qWarning() << "Image not found:" << imagePath; return; } m_imageCounter++; - ImageEntry entry; - entry.sourcePath = imagePath; - entry.ext = fi.suffix().toLower(); - entry.rId = QString("rId%1").arg(10 + m_imageCounter); - entry.index = m_imageCounter; - m_images.append(entry); + ImageEntry e; + e.sourcePath = imagePath; e.ext = fi.suffix().toLower(); + e.rId = QString("rId%1").arg(10+m_imageCounter); e.index = m_imageCounter; + m_images.append(e); - DocElement el; - el.type = DocElementType::Image; - el.imagePath = imagePath; - el.imageWidthEmu = widthEmu; - el.imageHeightEmu = heightEmu; - // сохраняем индекс в fontSize (хак для передачи индекса) + DocElement el; el.type = DocElementType::Image; el.imagePath = imagePath; + el.imageWidthEmu = widthEmu; el.imageHeightEmu = heightEmu; el.style.fontSize = m_imageCounter; m_elements.append(el); } void DocxBuilder::addPageBreak() { - DocElement el; - el.type = DocElementType::NormalText; - el.text = "\f"; // маркер разрыва страницы + DocElement el; el.type = DocElementType::NormalText; el.text = "\f"; m_elements.append(el); } -// ============================================================ -// Сохранение -// ============================================================ - bool DocxBuilder::save(const QString& filePath) { - QFile file(filePath); - if (!file.open(QIODevice::WriteOnly)) { - qWarning() << "DocxBuilder::save — не удалось открыть файл:" << filePath; - return false; - } + ZipWriter zip; + if (!zip.open(filePath)) { qWarning() << "Cannot open:" << filePath; return false; } - QZipWriter zip(&file); - zip.setCompressionPolicy(QZipWriter::AutoCompress); + zip.add("[Content_Types].xml", buildContentTypesXml().toUtf8()); + zip.add("_rels/.rels", buildRelsXml().toUtf8()); + zip.add("word/document.xml", buildDocumentXml().toUtf8()); + zip.add("word/_rels/document.xml.rels", buildDocumentRelsXml().toUtf8()); + zip.add("word/styles.xml", buildStylesXml().toUtf8()); + zip.add("word/settings.xml", buildSettingsXml().toUtf8()); - // --- Обязательные части DOCX --- - zip.addFile("[Content_Types].xml", buildContentTypesXml().toUtf8()); - zip.addFile("_rels/.rels", buildRelsXml().toUtf8()); - zip.addFile("word/document.xml", buildDocumentXml().toUtf8()); - zip.addFile("word/_rels/document.xml.rels", buildDocumentRelsXml().toUtf8()); - zip.addFile("word/styles.xml", buildStylesXml().toUtf8()); - zip.addFile("word/settings.xml", buildSettingsXml().toUtf8()); - - // --- Изображения --- for (const ImageEntry& img : m_images) { - QFile imgFile(img.sourcePath); - if (imgFile.open(QIODevice::ReadOnly)) { - QByteArray data = imgFile.readAll(); - zip.addFile(QString("word/media/image%1.%2").arg(img.index).arg(img.ext), data); - } + QFile f(img.sourcePath); + if (f.open(QIODevice::ReadOnly)) + zip.add(QString("word/media/image%1.%2").arg(img.index).arg(img.ext), f.readAll()); } - zip.close(); - file.close(); return true; } // ============================================================ -// Построение XML документа +// XML helpers // ============================================================ -QString DocxBuilder::buildDocumentXml() { - QString body; - - for (const DocElement& el : m_elements) { - switch (el.type) { - case DocElementType::Heading1: - body += makeParagraph(el.text, el.style, "Heading1"); - break; - case DocElementType::Heading2: - body += makeParagraph(el.text, el.style, "Heading2"); - break; - case DocElementType::Heading3: - body += makeParagraph(el.text, el.style, "Heading3"); - break; - case DocElementType::NormalText: - if (el.text == "\f") { - // Разрыв страницы - body += R"()"; - } else { - body += makeParagraph(el.text, el.style); - } - break; - case DocElementType::Table: - body += makeTable(el.table); - break; - case DocElementType::Image: { - // Найти соответствующую ImageEntry - int idx = el.style.fontSize; - ImageEntry* found = nullptr; - for (ImageEntry& img : m_images) { - if (img.index == idx) { found = &img; break; } - } - if (found) { - QString imgXml = makeImageXml( - found->rId.mid(3).toInt(), - el.imageWidthEmu, el.imageHeightEmu, idx); - body += QString("%1").arg(imgXml); - } - break; - } - } - } - - return QString( - R"()" - R"()" - "%1" - R"()" - "" - ).arg(body); +QString DocxBuilder::escapeXml(const QString& t) { + QString o=t; o.replace("&","&"); o.replace("<","<"); o.replace(">",">"); return o; } -// ============================================================ -// Вспомогательные генераторы XML -// ============================================================ - -QString DocxBuilder::escapeXml(const QString& text) { - QString out = text; - out.replace("&", "&"); - out.replace("<", "<"); - out.replace(">", ">"); - out.replace("\"", """); - out.replace("'", "'"); - return out; +QString DocxBuilder::makeRunProps(const DocStyle& s) { + return QString("" + "%3%4") + .arg(s.font).arg(s.fontSize) + .arg(s.bold ? "" : "") + .arg(s.italic ? "" : ""); } -QString DocxBuilder::makeRunProps(const DocStyle& style) { - QString rpr = ""; - rpr += QString("").arg(style.font); - rpr += QString("").arg(style.fontSize); - rpr += QString("").arg(style.fontSize); - if (style.bold) rpr += ""; - if (style.italic) rpr += ""; - rpr += ""; - return rpr; +QString DocxBuilder::makeParaProps(const DocStyle& s, const QString& styleId) { + QString p = ""; + if (!styleId.isEmpty()) p += QString("").arg(styleId); + if (s.center) p += ""; + if (styleId.isEmpty()) p += ""; + return p + ""; } -QString DocxBuilder::makeParaProps(const DocStyle& style, const QString& styleId) { - QString ppr = ""; - if (!styleId.isEmpty()) - ppr += QString("").arg(styleId); - if (style.center) - ppr += ""; - // Отступы для обычного текста - if (styleId.isEmpty()) - ppr += ""; - ppr += ""; - return ppr; -} - -QString DocxBuilder::makeParagraph(const QString& text, - const DocStyle& style, - const QString& styleId) -{ - // Разбиваем по переносам строки на отдельные параграфы - QStringList lines = text.split('\n'); - QString result; - for (int li = 0; li < lines.size(); ++li) { - result += ""; - result += makeParaProps(style, styleId); - result += ""; - result += makeRunProps(style); - result += QString("%1").arg(escapeXml(lines[li])); - result += ""; - result += ""; - } - return result; +QString DocxBuilder::makeParagraph(const QString& text, const DocStyle& s, const QString& styleId) { + QString r; + for (const QString& line : text.split('\n')) + r += "" + makeParaProps(s, styleId) + "" + makeRunProps(s) + + "" + escapeXml(line) + ""; + return r; } QString DocxBuilder::makeTable(const TableData& table) { - // Считаем количество колонок int cols = table.headers.isEmpty() - ? (table.rows.isEmpty() ? 1 : table.rows[0].size()) - : table.headers.size(); + ? (table.rows.isEmpty() ? 1 : table.rows[0].size()) : table.headers.size(); + int cw = (cols>0) ? 9356/cols : 9356; - // Ширина колонки: таблица на всю ширину страницы A4 (9356 DXA) - int colWidth = (cols > 0) ? (9356 / cols) : 9356; + QString t = "" + "" + "" + "" + "" + "" + "" + "" + ""; + for (int c=0;c").arg(cw); + t += ""; - QString tbl; - tbl += ""; - - // Свойства таблицы - tbl += ""; - tbl += ""; - tbl += ""; - tbl += "" - "" - "" - "" - "" - "" - "" - ""; - tbl += ""; - - // Сетка колонок - tbl += ""; - for (int c = 0; c < cols; ++c) - tbl += QString("").arg(colWidth); - tbl += ""; - - // Строка заголовков if (!table.headers.isEmpty()) { - tbl += ""; - for (const QString& hdr : table.headers) { - tbl += ""; - tbl += QString("" - "" - "").arg(colWidth); - tbl += "" - "" - "" - "" - ""; - tbl += QString("%1").arg(escapeXml(hdr)); - tbl += ""; + t += ""; + for (const QString& h : table.headers) { + t += QString("" + "" + "" + "" + "" + "%2").arg(cw).arg(escapeXml(h)); } - tbl += ""; + t += ""; } - - // Строки данных for (const QStringList& row : table.rows) { - tbl += ""; - for (int c = 0; c < cols; ++c) { - QString cellText = (c < row.size()) ? row[c] : ""; - tbl += ""; - tbl += QString("").arg(colWidth); - tbl += "" - "" - "" - ""; - tbl += QString("%1") - .arg(escapeXml(cellText)); - tbl += ""; + t += ""; + for (int c=0;c" + "" + "" + "%2") + .arg(cw).arg(escapeXml(ct)); } - tbl += ""; + t += ""; } - - tbl += ""; - // Пустой параграф после таблицы (обязателен по спецификации OOXML) - tbl += ""; - return tbl; + return t + ""; } -QString DocxBuilder::makeImageXml(int rId, int widthEmu, int heightEmu, int imgIdx) { - // EMU: 914400 = 1 дюйм +QString DocxBuilder::makeImageXml(int rId, int w, int h, int idx) { return QString( - "" - "" + "" "" "" - "" - "" - "" - "" + "" + "" "" "" "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - ).arg(widthEmu).arg(heightEmu).arg(imgIdx).arg(rId); + "" + "" + "" + "" + "" + "" + "" + ).arg(w).arg(h).arg(idx).arg(rId); } -// ============================================================ -// Системные XML-файлы DOCX -// ============================================================ +QString DocxBuilder::buildDocumentXml() { + QString body; + for (const DocElement& el : m_elements) { + switch (el.type) { + case DocElementType::Heading1: body += makeParagraph(el.text, el.style, "Heading1"); break; + case DocElementType::Heading2: body += makeParagraph(el.text, el.style, "Heading2"); break; + case DocElementType::Heading3: body += makeParagraph(el.text, el.style, "Heading3"); break; + case DocElementType::NormalText: + body += (el.text=="\f") ? QString(R"()") + : makeParagraph(el.text, el.style); + break; + case DocElementType::Table: body += makeTable(el.table); break; + case DocElementType::Image: + for (const ImageEntry& img : m_images) + if (img.index == el.style.fontSize) { + body += "" + + makeImageXml(img.rId.mid(3).toInt(), + el.imageWidthEmu, el.imageHeightEmu, img.index) + + ""; + break; + } + break; + } + } + return QString( + "" + "" + "%1" + "" + "" + "" + ).arg(body); +} QString DocxBuilder::buildContentTypesXml() { QString ct = @@ -367,28 +319,27 @@ QString DocxBuilder::buildContentTypesXml() { " ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml\"/>" ""; - - for (const ImageEntry& img : m_images) { - QString mime = (img.ext == "png") ? "image/png" : "image/jpeg"; - ct += QString("").arg(img.ext).arg(mime); - } - - ct += ""; - return ct; + QSet seen; + for (const ImageEntry& img : m_images) + if (!seen.contains(img.ext)) { + ct += QString("") + .arg(img.ext).arg(img.ext=="png" ? "image/png" : "image/jpeg"); + seen.insert(img.ext); + } + return ct + ""; } QString DocxBuilder::buildRelsXml() { - return - "" - "" - "" - ""; + return "" + "" + "" + ""; } QString DocxBuilder::buildDocumentRelsXml() { - QString rels = + QString r = "" "" ""; - - for (const ImageEntry& img : m_images) { - rels += QString( - "" - ).arg(img.rId).arg(img.index).arg(img.ext); - } - - rels += ""; - return rels; + for (const ImageEntry& img : m_images) + r += QString("") + .arg(img.rId).arg(img.index).arg(img.ext); + return r + ""; } QString DocxBuilder::buildSettingsXml() { - return - "" - "" - "" - "" - ""; + return "" + "" + "" + "" + ""; } QString DocxBuilder::buildStylesXml() { - // Определяем стили Times New Roman для заголовков и нормального текста return "" "" - // Normal "" "" - "" - "" - "" - "" - "" + "" + "" - // Heading 1 — Times New Roman 18pt Bold "" - "" - "" + "" "" - "" - "" - "" - "" - "" + "" + "" - // Heading 2 — Times New Roman 15pt Bold "" - "" - "" + "" "" - "" - "" - "" - "" - "" + "" + "" - // Heading 3 — Times New Roman 13pt Bold "" - "" - "" + "" "" - "" - "" - "" - "" - "" + "" + "" - // TableGrid "" - "" - "" - "" - "" - "" + "" + "" + "" "" - "" + "" "" "" - "" - "" - "" + "" ""; } diff --git a/DocxWizard.pro b/DocxWizard.pro index c67f36d..6face05 100644 --- a/DocxWizard.pro +++ b/DocxWizard.pro @@ -7,10 +7,6 @@ CONFIG += c++17 TARGET = DocxWizard TEMPLATE = app -# Подключаем приватные заголовки Qt для QZipWriter/QZipReader -# (входят в стандартную поставку Qt5) -QT += core-private - SOURCES += \ main.cpp \ DocxBuilder.cpp \ @@ -20,18 +16,8 @@ HEADERS += \ DocxBuilder.h \ DocumentWizard.h -# Если QZipWriter недоступен как private — используйте QuaZip: -# Установка: sudo apt install libquazip5-dev (Linux) -# или скачайте https://github.com/stachenov/quazip -# -# Тогда замените в DocxBuilder.cpp: -# #include -# #include -# на: -# #include -# #include -# и адаптируйте метод save() под QuaZip API. -# -# Раскомментируйте, если используете QuaZip: -# LIBS += -lquazip5 -# INCLUDEPATH += /usr/include/quazip5 +# zlib — входит в состав системы везде, где есть Qt +# Linux/macOS: подключается автоматически +# Windows (MSVC/MinGW): zlib поставляется вместе с Qt +unix: LIBS += -lz +win32: LIBS += -lzlib # при необходимости замените на -lz или укажите путь