#include "DocxBuilder.h" #include #include #include #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::addHeading(const QString& text, int level) { DocElement el; el.text = text; 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; 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() << "Image not found:" << imagePath; return; } m_imageCounter++; 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; 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) { ZipWriter zip; if (!zip.open(filePath)) { qWarning() << "Cannot open:" << filePath; return false; } 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()); for (const ImageEntry& img : m_images) { 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(); return true; } // ============================================================ // XML helpers // ============================================================ QString DocxBuilder::escapeXml(const QString& t) { QString o=t; o.replace("&","&"); o.replace("<","<"); o.replace(">",">"); return o; } QString DocxBuilder::makeRunProps(const DocStyle& s) { return QString("" "%3%4") .arg(s.font).arg(s.fontSize) .arg(s.bold ? "" : "") .arg(s.italic ? "" : ""); } 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::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(); int cw = (cols>0) ? 9356/cols : 9356; QString t = "" "" "" "" "" "" "" "" ""; for (int c=0;c").arg(cw); t += ""; if (!table.headers.isEmpty()) { t += ""; for (const QString& h : table.headers) { t += QString("" "" "" "" "" "%2").arg(cw).arg(escapeXml(h)); } t += ""; } for (const QStringList& row : table.rows) { t += ""; for (int c=0;c" "" "" "%2") .arg(cw).arg(escapeXml(ct)); } t += ""; } return t + ""; } QString DocxBuilder::makeImageXml(int rId, int w, int h, int idx) { return QString( "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ).arg(w).arg(h).arg(idx).arg(rId); } 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 = "" "" "" "" "" "" ""; 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 "" "" "" ""; } QString DocxBuilder::buildDocumentRelsXml() { QString r = "" "" "" ""; for (const ImageEntry& img : m_images) r += QString("") .arg(img.rId).arg(img.index).arg(img.ext); return r + ""; } QString DocxBuilder::buildSettingsXml() { return "" "" "" "" ""; } QString DocxBuilder::buildStylesXml() { return "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; }