#include "DocxBuilder.h" #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 // ============================================================ DocxBuilder::DocxBuilder() = default; 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.style.font = "Times New Roman"; el.style.bold = true; 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; m_elements.append(el); } void DocxBuilder::addTable(const TableData& 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; } 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); DocElement el; el.type = DocElementType::Image; el.imagePath = imagePath; el.imageWidthEmu = widthEmu; el.imageHeightEmu = heightEmu; // сохраняем индекс в fontSize (хак для передачи индекса) el.style.fontSize = m_imageCounter; m_elements.append(el); } void DocxBuilder::addPageBreak() { 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; } QZipWriter zip(&file); zip.setCompressionPolicy(QZipWriter::AutoCompress); // --- Обязательные части 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); } } zip.close(); file.close(); return true; } // ============================================================ // Построение XML документа // ============================================================ 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); } // ============================================================ // Вспомогательные генераторы 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& 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& 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::makeTable(const TableData& table) { // Считаем количество колонок int cols = table.headers.isEmpty() ? (table.rows.isEmpty() ? 1 : table.rows[0].size()) : table.headers.size(); // Ширина колонки: таблица на всю ширину страницы A4 (9356 DXA) int colWidth = (cols > 0) ? (9356 / cols) : 9356; 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 += ""; } tbl += ""; } // Строки данных 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 += ""; } tbl += ""; } tbl += ""; // Пустой параграф после таблицы (обязателен по спецификации OOXML) tbl += ""; return tbl; } QString DocxBuilder::makeImageXml(int rId, int widthEmu, int heightEmu, int imgIdx) { // EMU: 914400 = 1 дюйм return QString( "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ).arg(widthEmu).arg(heightEmu).arg(imgIdx).arg(rId); } // ============================================================ // Системные XML-файлы DOCX // ============================================================ QString DocxBuilder::buildContentTypesXml() { QString ct = "" "" "" "" "" "" ""; 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; } QString DocxBuilder::buildRelsXml() { return "" "" "" ""; } QString DocxBuilder::buildDocumentRelsXml() { QString rels = "" "" "" ""; for (const ImageEntry& img : m_images) { rels += QString( "" ).arg(img.rId).arg(img.index).arg(img.ext); } rels += ""; return rels; } QString DocxBuilder::buildSettingsXml() { 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 "" "" "" "" "" "" "" "" "" "" "" "" "" ""; }