diff --git a/PdfTool/PdfTool.pro b/PdfTool/PdfTool.pro index 4ed7ca7..43b3c0a 100644 --- a/PdfTool/PdfTool.pro +++ b/PdfTool/PdfTool.pro @@ -42,6 +42,7 @@ LIBS += -lPDFForQtLib SOURCES += \ main.cpp \ + pdfoutputformatter.cpp \ pdftoolabstractapplication.cpp # Default rules for deployment. @@ -54,4 +55,5 @@ application.path = $$DESTDIR/install INSTALLS += application HEADERS += \ + pdfoutputformatter.h \ pdftoolabstractapplication.h diff --git a/PdfTool/pdfoutputformatter.cpp b/PdfTool/pdfoutputformatter.cpp new file mode 100644 index 0000000..2836b46 --- /dev/null +++ b/PdfTool/pdfoutputformatter.cpp @@ -0,0 +1,571 @@ +// Copyright (C) 2020 Jakub Melka +// +// This file is part of PdfForQt. +// +// PdfForQt is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// PdfForQt is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with PDFForQt. If not, see . + +#include "pdfoutputformatter.h" + +#include +#include +#include + +#include + +namespace pdftool +{ + +class PDFOutputFormatterImpl +{ +public: + explicit PDFOutputFormatterImpl() = default; + virtual ~PDFOutputFormatterImpl() = default; + + /// Starts a new element in structure tree. Each call of this function must be + /// accompanied with matching call to \p endElement function. Element can have + /// internal name (in xml file), text description for user (as string), and reference + /// number. Also alignment can also be specified. Element type and name must + /// always be specified. + /// \param type Element type + /// \param name Internal element name (for example, xml tag if style is XML) + /// \param description Text description for user + /// \param alignment Cell alignment in table + /// \param reference Reference number + virtual void beginElement(PDFOutputFormatter::Element type, QString name, QString description = QString(), Qt::Alignment alignment = Qt::AlignLeft, int reference = 0) = 0; + + /// Ends current element. Must match with a call of \p beginElement + virtual void endElement() = 0; + + /// Get result string in unicode. + virtual QString getString() const = 0; +}; + +class PDFTextOutputFormatterImpl : public PDFOutputFormatterImpl +{ +public: + PDFTextOutputFormatterImpl(); + + virtual void beginElement(PDFOutputFormatter::Element type, QString name, QString description, Qt::Alignment alignment, int reference) override; + virtual void endElement() override; + virtual QString getString() const override; + +private: + static constexpr const int INDENT_STEP = 2; + + void writeIndent(); + + struct TableCell + { + QString text; + Qt::Alignment alignment; + }; + + const TableCell& getTableCell(size_t row, size_t column) const; + + QString m_string; + QTextStream m_streamWriter; + int m_depth; + int m_indent; + std::stack m_elementStack; + std::vector> m_table; +}; + +class PDFXmlOutputFormatterImpl : public PDFOutputFormatterImpl +{ +public: + PDFXmlOutputFormatterImpl(); + + virtual void beginElement(PDFOutputFormatter::Element type, QString name, QString description, Qt::Alignment alignment, int reference) override; + virtual void endElement() override; + virtual QString getString() const override; + +private: + QString m_string; + QString m_namespace; + QString m_prefix; + QXmlStreamWriter m_streamWriter; + int m_depth; +}; + +class PDFHtmlOutputFormatterImpl : public PDFOutputFormatterImpl +{ +public: + PDFHtmlOutputFormatterImpl(); + + virtual void beginElement(PDFOutputFormatter::Element type, QString name, QString description, Qt::Alignment alignment, int reference) override; + virtual void endElement() override; + virtual QString getString() const override; + +private: + QString m_string; + QXmlStreamWriter m_streamWriter; + int m_depth; + int m_headerDepth; + std::stack m_elementStack; +}; + +PDFTextOutputFormatterImpl::PDFTextOutputFormatterImpl() : + m_string(), + m_streamWriter(&m_string, QIODevice::WriteOnly), + m_depth(0), + m_elementStack() +{ + +} + +void PDFTextOutputFormatterImpl::beginElement(PDFOutputFormatter::Element type, QString name, QString description, Qt::Alignment alignment, int reference) +{ + Q_UNUSED(reference); + m_elementStack.push(type); + + switch (type) + { + case PDFOutputFormatter::Element::Root: + { + m_streamWriter << description << Qt::endl; + m_indent += INDENT_STEP; + break; + } + + case PDFOutputFormatter::Element::Table: + case PDFOutputFormatter::Element::Header: + { + writeIndent(); + m_streamWriter << description << Qt::endl; + m_indent += INDENT_STEP; + break; + } + + case PDFOutputFormatter::Element::TableRow: + case PDFOutputFormatter::Element::TableHeaderRow: + { + m_table.emplace_back(); + break; + } + + case PDFOutputFormatter::Element::TableHeaderColumn: + case PDFOutputFormatter::Element::TableColumn: + { + TableCell cell; + cell.text = QString(" %1 ").arg(description); + cell.alignment = alignment; + + Q_ASSERT(!m_table.empty()); + m_table.back().emplace_back(qMove(cell)); + break; + } + + default: + { + Q_ASSERT(false); + break; + } + } + + // Increment depth by one + ++m_depth; +} + +void PDFTextOutputFormatterImpl::endElement() +{ + PDFOutputFormatter::Element type = m_elementStack.top(); + m_elementStack.pop(); + --m_depth; + + switch (type) + { + case PDFOutputFormatter::Element::Root: + { + m_indent -= INDENT_STEP; + break; + } + + case PDFOutputFormatter::Element::Table: + { + // Print the table + const size_t rows = m_table.size(); + const size_t columns = (*std::max_element(m_table.cbegin(), m_table.cend(), [](const auto& l, const auto& r) { return l.size() < r.size(); })).size(); + + // Detect maximal column size + std::vector columnSize(columns, 0); + for (size_t row = 0; row < rows; ++row) + { + for (size_t column = 0; column < columns; ++column) + { + const TableCell& tableCell = getTableCell(row, column); + columnSize[column] = qMax(columnSize[column], tableCell.text.size()); + } + } + + // Print cells + m_streamWriter.setPadChar(QChar(QChar::Space)); + for (size_t row = 0; row < rows; ++row) + { + for (size_t column = 0; column < columns; ++column) + { + const TableCell& tableCell = getTableCell(row, column); + m_streamWriter.setFieldWidth(columnSize[column]); + + if (tableCell.alignment.testFlag(Qt::AlignLeft)) + { + m_streamWriter.setFieldAlignment(QTextStream::AlignLeft); + } + else if (tableCell.alignment.testFlag(Qt::AlignCenter)) + { + m_streamWriter.setFieldAlignment(QTextStream::AlignCenter); + } + else if (tableCell.alignment.testFlag(Qt::AlignRight)) + { + m_streamWriter.setFieldAlignment(QTextStream::AlignRight); + } + + m_streamWriter << tableCell.text; + } + + m_streamWriter.setFieldWidth(0); + m_streamWriter << Qt::endl; + } + + m_indent -= INDENT_STEP; + m_table.clear(); + break; + } + + case PDFOutputFormatter::Element::Header: + { + m_indent -= INDENT_STEP; + break; + } + + case PDFOutputFormatter::Element::TableRow: + case PDFOutputFormatter::Element::TableHeaderRow: + case PDFOutputFormatter::Element::TableHeaderColumn: + case PDFOutputFormatter::Element::TableColumn: + { + break; + } + + default: + { + Q_ASSERT(false); + break; + } + } +} + +QString PDFTextOutputFormatterImpl::getString() const +{ + return m_string; +} + +void PDFTextOutputFormatterImpl::writeIndent() +{ + QString str(m_indent, QChar(QChar::Space)); + m_streamWriter << str; +} + +const PDFTextOutputFormatterImpl::TableCell& PDFTextOutputFormatterImpl::getTableCell(size_t row, size_t column) const +{ + if (row < m_table.size()) + { + const auto& columns = m_table[row]; + if (column < columns.size()) + { + return columns[column]; + } + } + + static const TableCell dummy; + return dummy; +} + +PDFHtmlOutputFormatterImpl::PDFHtmlOutputFormatterImpl() : + m_string(), + m_streamWriter(&m_string), + m_depth(0), + m_headerDepth(1), + m_elementStack() +{ + +} + +void PDFHtmlOutputFormatterImpl::beginElement(PDFOutputFormatter::Element type, QString name, QString description, Qt::Alignment alignment, int reference) +{ + Q_UNUSED(reference); + m_elementStack.push(type); + + auto writeTableCellAlignment = [this, alignment]() + { + if (alignment.testFlag(Qt::AlignLeft)) + { + m_streamWriter.writeAttribute("align", "left"); + } + if (alignment.testFlag(Qt::AlignCenter)) + { + m_streamWriter.writeAttribute("align", "center"); + } + if (alignment.testFlag(Qt::AlignRight)) + { + m_streamWriter.writeAttribute("align", "right"); + } + }; + + switch (type) + { + case PDFOutputFormatter::Element::Root: + { + m_streamWriter.writeStartDocument(); + + QString title = QString("%1 - Processed by %2 %3").arg(description, QCoreApplication::applicationName(), QCoreApplication::applicationVersion()); + m_streamWriter.writeStartElement("html"); + m_streamWriter.writeStartElement("head"); + m_streamWriter.writeTextElement("title", title); + m_streamWriter.writeEndElement(); + m_streamWriter.writeStartElement("body"); + m_streamWriter.writeStartElement("p"); + m_streamWriter.writeTextElement("h1", title); + m_streamWriter.writeEndElement(); + break; + } + + case PDFOutputFormatter::Element::Header: + { + // Just print single paragraph with header + ++m_headerDepth; + int headerTagDepth = qBound(1, m_headerDepth, 6); + QString headerTag = QString("h%1").arg(headerTagDepth); + m_streamWriter.writeStartElement("p"); + m_streamWriter.writeTextElement(headerTag, description); + m_streamWriter.writeEndElement(); + break; + } + + case PDFOutputFormatter::Element::Table: + { + m_streamWriter.writeStartElement("table"); + break; + } + + case PDFOutputFormatter::Element::TableHeaderRow: + { + m_streamWriter.writeStartElement("tr"); + break; + } + + case PDFOutputFormatter::Element::TableHeaderColumn: + { + m_streamWriter.writeStartElement("th"); + writeTableCellAlignment(); + m_streamWriter.writeCharacters(description); + m_streamWriter.writeEndElement(); + break; + } + + case PDFOutputFormatter::Element::TableRow: + { + m_streamWriter.writeStartElement("tr"); + break; + } + + case PDFOutputFormatter::Element::TableColumn: + { + m_streamWriter.writeStartElement("td"); + writeTableCellAlignment(); + m_streamWriter.writeCharacters(description); + m_streamWriter.writeEndElement(); + break; + } + + default: + { + Q_ASSERT(false); + break; + } + } + + // Increment depth by one + ++m_depth; +} + +void PDFHtmlOutputFormatterImpl::endElement() +{ + PDFOutputFormatter::Element type = m_elementStack.top(); + m_elementStack.pop(); + --m_depth; + + switch (type) + { + case PDFOutputFormatter::Element::Root: + { + Q_ASSERT(m_depth == 0); + m_streamWriter.writeEndElement(); + m_streamWriter.writeEndElement(); + m_streamWriter.writeEndDocument(); + break; + } + + case PDFOutputFormatter::Element::Header: + { + // Just decrement header depth + --m_headerDepth; + break; + } + case PDFOutputFormatter::Element::Table: + case PDFOutputFormatter::Element::TableHeaderRow: + case PDFOutputFormatter::Element::TableRow: + { + m_streamWriter.writeEndElement(); + break; + } + + case PDFOutputFormatter::Element::TableHeaderColumn: + case PDFOutputFormatter::Element::TableColumn: + { + // Do nothing... + break; + } + + default: + { + Q_ASSERT(false); + break; + } + } +} + +QString PDFHtmlOutputFormatterImpl::getString() const +{ + QString html = m_string; + html.remove(0, html.indexOf("?>") + 2); + html.prepend(""); + return html; +} + +PDFXmlOutputFormatterImpl::PDFXmlOutputFormatterImpl() : + m_string(), + m_streamWriter(&m_string), + m_depth(0) +{ + m_streamWriter.setAutoFormatting(true); + m_streamWriter.setAutoFormattingIndent(2); + + m_namespace = "https://github.com/JakubMelka/PdfForQt"; + m_prefix = "pdftool"; +} + +void PDFXmlOutputFormatterImpl::beginElement(PDFOutputFormatter::Element type, QString name, QString description, Qt::Alignment alignment, int reference) +{ + Q_UNUSED(alignment); + + switch (type) + { + case PDFOutputFormatter::Element::Root: + { + m_streamWriter.writeStartDocument(); + + QString comment = QString("Processed by %1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()); + + m_streamWriter.writeComment(comment); + m_streamWriter.writeNamespace(m_namespace, m_prefix); + + m_streamWriter.writeStartElement(m_namespace, name); + break; + } + + case PDFOutputFormatter::Element::TableColumn: + case PDFOutputFormatter::Element::TableHeaderColumn: + { + m_streamWriter.writeTextElement(m_namespace, name, description); + break; + } + + default: + { + m_streamWriter.writeStartElement(m_namespace, name); + + if (!description.isEmpty()) + { + m_streamWriter.writeAttribute(m_namespace, "description", description); + } + + if (reference > 0) + { + m_streamWriter.writeAttribute(m_namespace, "ref", QString::number(reference)); + } + break; + } + } + + // Increment depth by one + ++m_depth; +} + +void PDFXmlOutputFormatterImpl::endElement() +{ + m_streamWriter.writeEndElement(); + + // Do we finish the document? If yes, then tell stream writer to end the document + if (--m_depth == 0) + { + m_streamWriter.writeEndDocument(); + } +} + +QString PDFXmlOutputFormatterImpl::getString() const +{ + return m_string; +} + +PDFOutputFormatter::PDFOutputFormatter(Style style) : + m_impl(nullptr) +{ + switch (style) + { + case Style::Text: + m_impl = new PDFTextOutputFormatterImpl(); + break; + + case Style::Xml: + m_impl = new PDFXmlOutputFormatterImpl(); + break; + + case Style::Html: + m_impl = new PDFHtmlOutputFormatterImpl(); + break; + } + + Q_ASSERT(m_impl); +} + +PDFOutputFormatter::~PDFOutputFormatter() +{ + delete m_impl; +} + +void PDFOutputFormatter::beginElement(PDFOutputFormatter::Element type, QString name, QString description, Qt::Alignment alignment, int reference) +{ + m_impl->beginElement(type, name, description, alignment, reference); +} + +void PDFOutputFormatter::endElement() +{ + m_impl->endElement(); +} + +QString PDFOutputFormatter::getString() const +{ + return m_impl->getString(); +} + +} // pdftool diff --git a/PdfTool/pdfoutputformatter.h b/PdfTool/pdfoutputformatter.h new file mode 100644 index 0000000..cae9b15 --- /dev/null +++ b/PdfTool/pdfoutputformatter.h @@ -0,0 +1,78 @@ +// Copyright (C) 2020 Jakub Melka +// +// This file is part of PdfForQt. +// +// PdfForQt is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// PdfForQt is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with PDFForQt. If not, see . + +#ifndef PDFOUTPUTFORMATTER_H +#define PDFOUTPUTFORMATTER_H + +#include + +namespace pdftool +{ +class PDFOutputFormatterImpl; + +/// Output formatter for text output in various format (text, xml, html...) +/// to the output console. Text output is in form of a structure tree, +/// for example, xml format. +class PDFOutputFormatter +{ +public: + enum class Style + { + Text, + Xml, + Html + }; + + explicit PDFOutputFormatter(Style style); + ~PDFOutputFormatter(); + + enum class Element + { + Root, ///< Root element, this must be used only once at start/end of writing + Header, ///< Header + Table, ///< Table of rows/columns (2D grid) + TableHeaderRow, ///< Table header row (consists of columns) + TableHeaderColumn, ///< Table header column + TableRow, ///< Table row (consists of columns) + TableColumn ///< Table column + }; + + /// Starts a new element in structure tree. Each call of this function must be + /// accompanied with matching call to \p endElement function. Element can have + /// internal name (in xml file), text description for user (as string), and reference + /// number. Also alignment can also be specified. Element type and name must + /// always be specified. + /// \param type Element type + /// \param name Internal element name (for example, xml tag if style is XML) + /// \param description Text description for user + /// \param alignment Cell alignment in table + /// \param reference Reference number + void beginElement(Element type, QString name, QString description = QString(), Qt::Alignment alignment = Qt::AlignLeft, int reference = 0); + + /// Ends current element. Must match with a call of \p beginElement + void endElement(); + + /// Get result string in unicode. + QString getString() const; + +private: + PDFOutputFormatterImpl* m_impl; +}; + +} // namespace pdftool + +#endif // PDFOUTPUTFORMATTER_H