From 7167ce41b61d2ba2cdb526777a4233eb84a3b66a Mon Sep 17 00:00:00 2001 From: Unit 193 Date: Sat, 6 Dec 2014 17:33:25 -0500 Subject: Imported Upstream version 2.99.6 --- Plugins/PdfExport/pdfexport.cpp | 1420 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1420 insertions(+) create mode 100644 Plugins/PdfExport/pdfexport.cpp (limited to 'Plugins/PdfExport/pdfexport.cpp') diff --git a/Plugins/PdfExport/pdfexport.cpp b/Plugins/PdfExport/pdfexport.cpp new file mode 100644 index 0000000..da15ab7 --- /dev/null +++ b/Plugins/PdfExport/pdfexport.cpp @@ -0,0 +1,1420 @@ +#include "pdfexport.h" +#include "common/unused.h" +#include "uiutils.h" +#include "log.h" +#include +#include +#include +#include + +QString PdfExport::bulletChar = "\u2022"; + +bool PdfExport::init() +{ + textOption = new QTextOption(); + textOption->setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + return GenericExportPlugin::init(); +} + +void PdfExport::deinit() +{ + safe_delete(textOption); +} + +QPagedPaintDevice* PdfExport::createPaintDevice(const QString& documentTitle) +{ + QPdfWriter* pdfWriter = new QPdfWriter(output); + pdfWriter->setTitle(documentTitle); + pdfWriter->setCreator(tr("SQLiteStudio v%1").arg(SQLITESTUDIO->getVersionString())); + return pdfWriter; +} + +QString PdfExport::getFormatName() const +{ + return "PDF"; +} + +ExportManager::StandardConfigFlags PdfExport::standardOptionsToEnable() const +{ + return 0; +} + +ExportManager::ExportProviderFlags PdfExport::getProviderFlags() const +{ + return ExportManager::DATA_LENGTHS|ExportManager::ROW_COUNT; +} + +void PdfExport::validateOptions() +{ +} + +QString PdfExport::defaultFileExtension() const +{ + return "pdf"; +} + +bool PdfExport::beforeExportQueryResults(const QString& query, QList& columns, const QHash providedData) +{ + UNUSED(query); + + if (!beginDoc(tr("SQL query results"))) + return false; + + totalRows = providedData[ExportManager::ROW_COUNT].toInt(); + + QStringList columnNames; + for (const QueryExecutor::ResultColumnPtr& col : columns) + columnNames << col->displayName; + + clearDataHeaders(); + exportDataColumnsHeader(columnNames); + + QList columnDataLengths = getColumnDataLengths(columnNames.size(), providedData); + calculateDataColumnWidths(columnNames, columnDataLengths); + return true; +} + +bool PdfExport::exportQueryResultsRow(SqlResultsRowPtr row) +{ + exportDataRow(row->valueList()); + return true; +} + +bool PdfExport::exportTable(const QString& database, const QString& table, const QStringList& columnNames, const QString& ddl, SqliteCreateTablePtr createTable, const QHash providedData) +{ + UNUSED(columnNames); + UNUSED(database); + UNUSED(ddl); + + if (isTableExport() && !beginDoc(tr("Exported table: %1").arg(table))) + return false; + + exportObjectHeader(tr("Table: %1").arg(table)); + + QStringList tableDdlColumns = {tr("Column"), tr("Data type"), tr("Constraints")}; + exportObjectColumnsHeader(tableDdlColumns); + + QString colDef; + QString colType; + QStringList columnsAndTypes; + int colNamesLength = 0; + int dataTypeLength = 0; + for (SqliteCreateTable::Column* col : createTable->columns) + { + colDef = col->name; + colNamesLength = qMax(colNamesLength, colDef.size()); + colType = ""; + if (col->type) + { + colType = col->type->toDataType().toFullTypeString(); + colDef += "\n" + colType; + dataTypeLength = qMax(dataTypeLength, colType.size()); + } + + columnsAndTypes << colDef; + } + + QList columnDataLengths = {colNamesLength, dataTypeLength, 0}; + calculateDataColumnWidths(tableDdlColumns, columnDataLengths, 2); + + for (SqliteCreateTable::Column* col : createTable->columns) + exportTableColumnRow(col); + + if (createTable->constraints.size() > 0) + { + QStringList tableDdlColumns = {tr("Global table constraints")}; + exportObjectColumnsHeader(tableDdlColumns); + exportTableConstraintsRow(createTable->constraints); + } + + flushObjectPages(); + + prepareTableDataExport(table, columnsAndTypes, providedData); + return true; +} + +bool PdfExport::exportVirtualTable(const QString& database, const QString& table, const QStringList& columnNames, const QString& ddl, SqliteCreateVirtualTablePtr createTable, const QHash providedData) +{ + UNUSED(columnNames); + UNUSED(database); + UNUSED(ddl); + UNUSED(createTable); + + if (isTableExport() && !beginDoc(tr("Exported table: %1").arg(table))) + return false; + + prepareTableDataExport(table, columnNames, providedData); + return true; +} + +void PdfExport::prepareTableDataExport(const QString& table, const QStringList& columnNames, const QHash providedData) +{ + resetDataTable(); + totalRows = providedData[ExportManager::ROW_COUNT].toInt(); + + // Prepare for exporting data row + clearDataHeaders(); + if (!isTableExport()) // for database export we need to mark what is this object name + exportDataHeader(tr("Table: %1").arg(table)); + + exportDataColumnsHeader(columnNames); + + QList columnDataLengths = getColumnDataLengths(columnNames.size(), providedData); + calculateDataColumnWidths(columnNames, columnDataLengths); +} + +QList PdfExport::getColumnDataLengths(int columnCount, const QHash providedData) +{ + QList columnDataLengths = providedData[ExportManager::DATA_LENGTHS].value>(); + if (columnDataLengths.size() < columnCount) + { + qWarning() << "PdfExport: column widths provided by ExportWorker (" << columnDataLengths.size() + << ") is less than number of columns to export (" << columnCount << ")."; + } + + // Fill up column data widths if there are any missing from the provided data (should not happen) + while (columnDataLengths.size() < columnCount) + columnDataLengths << maxColWidth; + + for (int& val : columnDataLengths) + { + if (val > cellDataLimit) + val = cellDataLimit; + } + + return columnDataLengths; +} + +bool PdfExport::exportTableRow(SqlResultsRowPtr data) +{ + exportDataRow(data->valueList()); + return true; +} + +bool PdfExport::afterExport() +{ + endDoc(); + return true; +} + +bool PdfExport::afterExportTable() +{ + flushDataPages(true); + return true; +} + +bool PdfExport::afterExportQueryResults() +{ + flushDataPages(true); + return true; +} + +bool PdfExport::beforeExportDatabase(const QString& database) +{ + return beginDoc(tr("Exported database: %1").arg(database)); +} + +bool PdfExport::exportIndex(const QString& database, const QString& name, const QString& ddl, SqliteCreateIndexPtr createIndex) +{ + UNUSED(database); + UNUSED(ddl); + + exportObjectHeader(tr("Index: %1").arg(name)); + + QStringList indexColumns = {tr("Property", "index header"), tr("Value", "index header")}; + exportObjectColumnsHeader(indexColumns); + + exportObjectRow({tr("Indexed table"), name}); + exportObjectRow({tr("Unique index"), (createIndex->uniqueKw ? tr("Yes") : tr("No"))}); + + indexColumns = {tr("Column"), tr("Collation"), tr("Sort order")}; + exportObjectColumnsHeader(indexColumns); + + QString sort; + for (SqliteIndexedColumn* idxCol : createIndex->indexedColumns) + { + if (idxCol->sortOrder != SqliteSortOrder::null) + sort = sqliteSortOrder(idxCol->sortOrder); + else + sort = ""; + + exportObjectRow({idxCol->name, idxCol->collate, sort}); + } + + if (createIndex->where) + { + indexColumns = {tr("Partial index condition")}; + exportObjectColumnsHeader(indexColumns); + exportObjectRow(createIndex->where->detokenize()); + } + + flushObjectPages(); + return true; +} + +bool PdfExport::exportTrigger(const QString& database, const QString& name, const QString& ddl, SqliteCreateTriggerPtr createTrigger) +{ + UNUSED(database); + UNUSED(ddl); + + exportObjectHeader(tr("Trigger: %1").arg(name)); + + QStringList trigColumns = {tr("Property", "trigger header"), tr("Value", "trigger header")}; + exportObjectColumnsHeader(trigColumns); + exportObjectRow({tr("Activation time"), SqliteCreateTrigger::time(createTrigger->eventTime)}); + + QString event = createTrigger->event ? SqliteCreateTrigger::Event::typeToString(createTrigger->event->type) : ""; + exportObjectRow({tr("For action"), event}); + + QString onObj; + if (createTrigger->eventTime == SqliteCreateTrigger::Time::INSTEAD_OF) + onObj = tr("On view"); + else + onObj = tr("On table"); + + exportObjectRow({onObj, createTrigger->table}); + + QString cond = createTrigger->precondition ? createTrigger->precondition->detokenize() : ""; + exportObjectRow({tr("Activation condition"), cond}); + + QStringList queryStrings; + for (SqliteQuery* q : createTrigger->queries) + queryStrings << q->detokenize(); + + exportObjectColumnsHeader({tr("Code executed")}); + exportObjectRow(queryStrings.join("\n")); + + flushObjectPages(); + return true; +} + +bool PdfExport::exportView(const QString& database, const QString& name, const QString& ddl, SqliteCreateViewPtr view) +{ + UNUSED(database); + UNUSED(ddl); + + exportObjectHeader(tr("View: %1").arg(name)); + exportObjectColumnsHeader({tr("Query:")}); + exportObjectRow(view->select->detokenize()); + + flushObjectPages(); + return true; +} + +bool PdfExport::isBinaryData() const +{ + return true; +} + +bool PdfExport::beginDoc(const QString& title) +{ + safe_delete(painter); + safe_delete(pagedWriter); + pagedWriter = createPaintDevice(title); + if (!pagedWriter) + return false; + + painter = new QPainter(pagedWriter); + painter->setBrush(Qt::NoBrush); + painter->setPen(QPen(Qt::black, lineWidth)); + + setupConfig(); + return true; +} + +void PdfExport::endDoc() +{ + drawFooter(); +} + +void PdfExport::cleanupAfterExport() +{ + safe_delete(painter); + safe_delete(pagedWriter); +} + +void PdfExport::setupConfig() +{ + pagedWriter->setPageSize(convertPageSize(cfg.PdfExport.PageSize.get())); + pageWidth = pagedWriter->width(); + pageHeight = pagedWriter->height(); + pointsPerMm = pageWidth / pagedWriter->pageSizeMM().width(); + + stdFont = cfg.PdfExport.Font.get(); + stdFont.setPointSize(cfg.PdfExport.FontSize.get()); + boldFont = stdFont; + boldFont.setBold(true); + italicFont = stdFont; + italicFont.setItalic(true); + painter->setFont(stdFont); + + topMargin = mmToPoints(cfg.PdfExport.TopMargin.get()); + rightMargin = mmToPoints(cfg.PdfExport.RightMargin.get()); + leftMargin = mmToPoints(cfg.PdfExport.LeftMargin.get()); + bottomMargin = mmToPoints(cfg.PdfExport.BottomMargin.get()); + updateMargins(); + + maxColWidth = pageWidth / 5; + padding = mmToPoints(cfg.PdfExport.Padding.get()); + + QRectF rect = painter->boundingRect(QRectF(padding, padding, pageWidth - 2 * padding, 1), "X", *textOption); + minRowHeight = rect.height() + padding * 2; + maxRowHeight = qMax((int)(pageHeight * 0.225), minRowHeight); + rowsToPrebuffer = (int)qCeil((double)pageHeight / minRowHeight); + + cellDataLimit = cfg.PdfExport.MaxCellBytes.get(); + printRowNum = cfg.PdfExport.PrintRowNum.get(); + printPageNumbers = cfg.PdfExport.PrintPageNumbers.get(); + + lastRowY = getContentsTop(); + currentPage = -1; + rowNum = 1; +} + +void PdfExport::updateMargins() +{ + pageWidth -= (leftMargin + rightMargin); + pageHeight -= (topMargin + bottomMargin); + painter->setClipRect(QRect(leftMargin, topMargin, pageWidth, pageHeight)); + + if (printPageNumbers) + { + int pageNumHeight = getPageNumberHeight(); + bottomMargin += pageNumHeight; + pageHeight -= pageNumHeight; + } + + // In order to render full width of the line, we need to add more margin, a half of the line width + leftMargin += lineWidth / 2; + rightMargin += lineWidth / 2; + topMargin += lineWidth / 2; + bottomMargin += lineWidth / 2; + pageWidth -= lineWidth; + pageHeight -= lineWidth; +} + +void PdfExport::clearDataHeaders() +{ + headerRow.reset(); + columnsHeaderRow.reset(); +} + +void PdfExport::resetDataTable() +{ + clearDataHeaders(); + bufferedDataRows.clear(); + rowNum = 0; +} + +void PdfExport::exportDataRow(const QList& data) +{ + DataCell cell; + DataRow row; + + for (const QVariant& value : data) + { + switch (value.type()) + { + case QVariant::Int: + case QVariant::UInt: + case QVariant::LongLong: + case QVariant::ULongLong: + case QVariant::Double: + cell.alignment = Qt::AlignRight; + break; + default: + cell.alignment = Qt::AlignLeft; + break; + } + + if (value.isNull()) + { + cell.alignment = Qt::AlignCenter; + cell.isNull = true; + cell.contents = QStringLiteral("NULL"); + } + else + { + cell.isNull = false; + cell.contents = value.toString(); + } + row.cells << cell; + } + + bufferedDataRows << row; + checkForDataRender(); +} + +void PdfExport::exportObjectHeader(const QString& contents) +{ + ObjectRow row; + ObjectCell cell; + cell.headerBackground = true; + cell.contents << contents; + cell.bold = true; + cell.alignment = Qt::AlignCenter; + row.cells << cell; + + row.type = ObjectRow::Type::SINGLE; + row.recalculateColumnWidths = true; + bufferedObjectRows << row; +} + +void PdfExport::exportObjectColumnsHeader(const QStringList& columns) +{ + ObjectRow row; + ObjectCell cell; + + for (const QString& col : columns) + { + cell.headerBackground = true; + cell.contents.clear(); + cell.contents << col; + cell.alignment = Qt::AlignCenter; + row.cells << cell; + } + + row.recalculateColumnWidths = true; + row.type = ObjectRow::Type::MULTI; + bufferedObjectRows << row; +} + +void PdfExport::exportTableColumnRow(SqliteCreateTable::Column* column) +{ + ObjectRow row; + row.type = ObjectRow::Type::MULTI; + + ObjectCell cell; + cell.contents << column->name; + row.cells << cell; + cell.contents.clear(); + + if (column->type) + cell.contents << column->type->toDataType().toFullTypeString(); + else + cell.contents << ""; + + row.cells << cell; + cell.contents.clear(); + + if (column->constraints.size() > 0) + { + for (SqliteCreateTable::Column::Constraint* constr : column->constraints) + cell.contents << constr->detokenize(); + + cell.type = ObjectCell::Type::LIST; + } + else + { + cell.contents << ""; + } + row.cells << cell; + cell.contents.clear(); + + bufferedObjectRows << row; +} + +void PdfExport::exportTableConstraintsRow(const QList& constrList) +{ + ObjectRow row; + row.type = ObjectRow::Type::SINGLE; + + ObjectCell cell; + cell.type = ObjectCell::Type::LIST; + + if (constrList.size() > 0) + { + for (SqliteCreateTable::Constraint* constr : constrList) + cell.contents << constr->detokenize(); + } + else + { + cell.contents << ""; + } + row.cells << cell; + + bufferedObjectRows << row; +} + +void PdfExport::exportObjectRow(const QStringList& values) +{ + ObjectRow row; + row.type = ObjectRow::Type::MULTI; + + ObjectCell cell; + for (const QString& value : values) + { + cell.contents << value; + row.cells << cell; + cell.contents.clear(); + } + + bufferedObjectRows << row; +} + +void PdfExport::exportObjectRow(const QString& value) +{ + ObjectRow row; + row.type = ObjectRow::Type::SINGLE; + + ObjectCell cell; + cell.contents << value; + row.cells << cell; + + bufferedObjectRows << row; +} + +int PdfExport::calculateRowHeight(int maxTextWidth, const QStringList& listContents) +{ + int textWidth = maxTextWidth - calculateBulletPrefixWidth(); + int totalHeight = 0; + for (const QString& contents : listContents) + totalHeight += calculateRowHeight(textWidth, contents); + + return totalHeight; +} + +int PdfExport::calculateBulletPrefixWidth() +{ + static QString prefix = bulletChar + " "; + + QTextOption opt = *textOption; + opt.setWrapMode(QTextOption::NoWrap); + + return painter->boundingRect(QRect(0, 0, 1, 1), prefix, *textOption).width(); +} + +void PdfExport::checkForDataRender() +{ + if (bufferedDataRows.size() >= rowsToPrebuffer) + flushDataPages(); +} + +void PdfExport::flushObjectPages() +{ + if (bufferedObjectRows.isEmpty()) + return; + + int y = getContentsTop(); + int totalHeight = lastRowY - y; + + if (totalHeight > 0) + { + totalHeight += minRowHeight * 2; // a space between objects on one page + y += totalHeight; + } + else + newPage(); + + while (!bufferedObjectRows.isEmpty()) + { + ObjectRow& row = bufferedObjectRows.first(); + + if (row.recalculateColumnWidths || row.cells.size() != calculatedObjectColumnWidths.size()) + calculateObjectColumnWidths(); + + totalHeight += row.height; + if (totalHeight > pageHeight) + { + newPage(); + y = getContentsTop(); + totalHeight = row.height; + } + flushObjectRow(row, y); + + y += row.height; + + bufferedObjectRows.removeFirst(); + } + + lastRowY = getContentsTop() + totalHeight; +} + +void PdfExport::drawObjectTopLine(int y) +{ + painter->drawLine(getContentsLeft(), y, getContentsRight(), y); +} + +void PdfExport::drawObjectCellHeaderBackground(int x1, int y1, int x2, int y2) +{ + painter->save(); + painter->setBrush(QBrush(cfg.PdfExport.HeaderBgColor.get(), Qt::SolidPattern)); + painter->setPen(Qt::NoPen); + painter->drawRect(x1, y1, x2 - x1, y2 - y1); + painter->restore(); +} + +void PdfExport::drawFooter() +{ + QString footer = tr("Document generated with SQLiteStudio v%1").arg(SQLITESTUDIO->getVersionString()); + + QTextOption opt = *textOption; + opt.setAlignment(Qt::AlignRight); + + int y = lastRowY + minRowHeight; + int height = pageHeight - y; + int txtHeight = painter->boundingRect(QRect(0, 0, pageWidth, height), footer, opt).height(); + if ((y + txtHeight) > pageHeight) + { + newPage(); + y = getContentsTop(); + } + + painter->save(); + painter->setFont(italicFont); + painter->drawText(QRect(getContentsLeft(), y, pageWidth, txtHeight), footer, opt); + painter->restore(); +} + +void PdfExport::flushObjectRow(const PdfExport::ObjectRow& row, int y) +{ + painter->save(); + int x = getContentsLeft(); + int bottom = y + row.height; + int top = y; + int left = getContentsLeft(); + int right = getContentsRight(); + switch (row.type) + { + case ObjectRow::Type::SINGLE: + { + const ObjectCell& cell = row.cells.first(); + if (cell.headerBackground) + drawObjectCellHeaderBackground(left, y, right, bottom); + + painter->drawLine(left, y, left, bottom); + painter->drawLine(right, y, right, bottom); + painter->drawLine(left, top, right, top); + painter->drawLine(left, bottom, right, bottom); + + flushObjectCell(cell, left, y, pageWidth, row.height); + break; + } + case ObjectRow::Type::MULTI: + { + int width = 0; + for (int col = 0, total = calculatedObjectColumnWidths.size(); col < total; ++col) + { + width = calculatedObjectColumnWidths[col]; + if (row.cells[col].headerBackground) + drawObjectCellHeaderBackground(x, y, x + width, bottom); + + x += width; + } + + x = left; + painter->drawLine(x, y, x, bottom); + for (int w : calculatedObjectColumnWidths) + { + x += w; + painter->drawLine(x, y, x, bottom); + } + painter->drawLine(left, top, right, top); + painter->drawLine(left, bottom, right, bottom); + + x = left; + for (int col = 0, total = calculatedObjectColumnWidths.size(); col < total; ++col) + { + const ObjectCell& cell = row.cells[col]; + width = calculatedObjectColumnWidths[col]; + flushObjectCell(cell, x, y, width, row.height); + x += width; + } + break; + } + } + painter->restore(); +} + +void PdfExport::flushObjectCell(const PdfExport::ObjectCell& cell, int x, int y, int w, int h) +{ + QTextOption opt = *textOption; + opt.setAlignment(cell.alignment); + + if (cell.bold) + painter->setFont(boldFont); + else if (cell.italic) + painter->setFont(italicFont); + + switch (cell.type) + { + case ObjectCell::Type::NORMAL: + { + painter->drawText(QRect(x + padding, y + padding, w - 2 * padding, h - 2 * padding), cell.contents.first(), opt); + break; + } + case ObjectCell::Type::LIST: + { + static QString prefix = bulletChar + " "; + int prefixWidth = calculateBulletPrefixWidth(); + x += padding; + y += padding; + w -= 2 * padding; + h -= 2 * padding; + int txtX = x + prefixWidth; + int txtW = w - prefixWidth; + + QTextOption prefixOpt = opt; + prefixOpt.setAlignment(opt.alignment() | Qt::AlignTop); + + int txtH = 0; + for (const QString& contents : cell.contents) + { + txtH = calculateRowHeight(txtW, contents); + painter->drawText(QRect(x, y, prefixWidth, txtH), prefix, prefixOpt); + painter->drawText(QRect(txtX, y, txtW, txtH), contents, opt); + y += txtH; + } + break; + } + } +} + +void PdfExport::calculateObjectColumnWidths(int columnToExpand) +{ + calculatedObjectColumnWidths.clear(); + if (bufferedObjectRows.size() == 0) + return; + + QTextOption opt = *textOption; + opt.setWrapMode(QTextOption::NoWrap); + + int colCount = bufferedObjectRows.first().cells.size(); + for (int i = 0; i < colCount; i++) + calculatedObjectColumnWidths << 0; + + int width = 0; + for (const ObjectRow& row : bufferedObjectRows) + { + if (row.cells.size() != colCount) + break; + + for (int col = 0; col < colCount; col++) + { + width = painter->boundingRect(QRectF(0, 0, 1, 1), row.cells[col].contents.join("\n"), opt).width(); + width += 2 * padding; + calculatedObjectColumnWidths[col] = qMax(calculatedObjectColumnWidths[col], width); + } + } + + int totalWidth = correctMaxObjectColumnWidths(colCount, columnToExpand); + if (totalWidth < pageWidth) + { + int col = (columnToExpand > -1) ? columnToExpand : (colCount - 1); + calculatedObjectColumnWidths[col] += (pageWidth - totalWidth); + } + + calculateObjectRowHeights(); +} + +int PdfExport::correctMaxObjectColumnWidths(int colCount, int columnToExpand) +{ + int totalWidth = 0; + for (int w : calculatedObjectColumnWidths) + totalWidth += w; + + int maxWidth = pageWidth / colCount; + if (totalWidth <= pageWidth) + return totalWidth; + + int tmpWidth = 0; + for (int col = 0; col < colCount && totalWidth > pageWidth; col++) + { + if (calculatedObjectColumnWidths[col] <= maxWidth) + continue; + + if (col == columnToExpand) + continue; // will handle that column as last one (if needed) + + tmpWidth = calculatedObjectColumnWidths[col]; + if ((totalWidth - calculatedObjectColumnWidths[col] + maxWidth) <= pageWidth) + { + calculatedObjectColumnWidths[col] -= (pageWidth - totalWidth + calculatedObjectColumnWidths[col] - maxWidth); + return pageWidth; // the 'if' condition guarantees that shrinking this column that much will give us pageWidth + } + else + calculatedObjectColumnWidths[col] = maxWidth; + + totalWidth -= tmpWidth - calculatedObjectColumnWidths[col]; + } + + if (columnToExpand > -1 && totalWidth > pageWidth) + { + tmpWidth = calculatedObjectColumnWidths[columnToExpand]; + if ((totalWidth - calculatedObjectColumnWidths[columnToExpand] + maxWidth) <= pageWidth) + calculatedObjectColumnWidths[columnToExpand] -= (pageWidth - totalWidth + calculatedObjectColumnWidths[columnToExpand] - maxWidth); + else + calculatedObjectColumnWidths[columnToExpand] = maxWidth; + } + + return pageWidth; +} + +void PdfExport::calculateObjectRowHeights() +{ + int maxHeight = 0; + int colWidth = 0; + int height = 0; + int colCount = calculatedObjectColumnWidths.size(); + for (ObjectRow& row : bufferedObjectRows) + { + if (row.cells.size() != colCount) + return; // stop at this row, further calculation will be done when columns are recalculated + + maxHeight = 0; + for (int col = 0; col < colCount; ++col) + { + colWidth = calculatedObjectColumnWidths[col]; + const ObjectCell& cell = row.cells[col]; + + switch (cell.type) + { + case ObjectCell::Type::NORMAL: + height = calculateRowHeight(colWidth, cell.contents.first()); + break; + case ObjectCell::Type::LIST: + height = calculateRowHeight(colWidth, cell.contents); + break; + } + maxHeight = qMax(maxHeight, height); + } + + row.height = qMin(maxRowHeight, maxHeight); + } +} + +void PdfExport::flushDataPages(bool forceRender) +{ + calculateDataRowHeights(); + + int rowsToRender = 0; + int totalRowHeight = 0; + int colStartAt = 0; + while ((bufferedDataRows.size() >= rowsToPrebuffer) || (forceRender && bufferedDataRows.size() > 0)) + { + // Calculate how many rows we can render on single page + rowsToRender = 0; + totalRowHeight = totalHeaderRowsHeight; + for (const DataRow& row : bufferedDataRows) + { + rowsToRender++; + totalRowHeight += row.height; + if (totalRowHeight >= pageHeight) + { + rowsToRender--; + break; + } + } + + // Render limited number of columns and rows per single page + colStartAt = 0; + for (int cols : columnsPerPage) + { + newPage(); + flushDataRowsPage(colStartAt, colStartAt + cols, rowsToRender); + colStartAt += cols; + } + + for (int i = 0; i < rowsToRender; i++) + bufferedDataRows.removeFirst(); + + rowNum += rowsToRender; + } +} + +void PdfExport::flushDataRowsPage(int columnStart, int columnEndBefore, int rowsToRender) +{ + QList allRows; + if (headerRow) + allRows += *headerRow; + + if (columnsHeaderRow) + allRows += *columnsHeaderRow; + + allRows += bufferedDataRows.mid(0, rowsToRender); + + int left = getContentsLeft(); + int right = getContentsRight(); + int top = getContentsTop(); + + // Calculating width of all columns on this page + int totalColumnsWidth = sum(calculatedDataColumnWidths.mid(columnStart, columnEndBefore - columnStart)); + int totalColumnsWidthWithRowId = totalColumnsWidth + rowNumColumnWidth; + + // Calculating height of all rows + int totalRowsHeight = 0; + for (const DataRow& row : allRows) + totalRowsHeight += row.height; + + // Draw header background + int x = getDataColumnsStartX(); + painter->save(); + painter->setBrush(QBrush(cfg.PdfExport.HeaderBgColor.get(), Qt::SolidPattern)); + painter->setPen(Qt::NoPen); + painter->drawRect(QRect(x, top, totalColumnsWidth, totalHeaderRowsHeight)); + painter->restore(); + + // Draw rowNum background + if (printRowNum) + { + painter->save(); + painter->setBrush(QBrush(cfg.PdfExport.HeaderBgColor.get(), Qt::SolidPattern)); + painter->setPen(Qt::NoPen); + painter->drawRect(QRect(left, top, rowNumColumnWidth, totalRowsHeight)); + painter->restore(); + } + + // Draw horizontal lines + int y = top; + int horizontalLineEnd = x + totalColumnsWidth; + painter->drawLine(left, y, horizontalLineEnd, y); + for (const DataRow& row : allRows) + { + y += row.height; + painter->drawLine(left, y, horizontalLineEnd, y); + } + + // Draw dashed horizontal lines if there are more columns on the next page and there is space on the right side + if (columnEndBefore < calculatedDataColumnWidths.size() && horizontalLineEnd < right) + { + y = top; + painter->save(); + QPen pen(Qt::lightGray, lineWidth, Qt::DashLine); + pen.setDashPattern(QVector({5.0, 3.0})); + painter->setPen(pen); + painter->drawLine(horizontalLineEnd, y, right, y); + for (const DataRow& row : allRows) + { + y += row.height; + painter->drawLine(horizontalLineEnd, y, right, y); + } + painter->restore(); + } + + // Finding first row to start vertical lines from. It's either a COLUMNS_HEADER, or first data row, after headers. + int verticalLinesStart = top; + if (headerRow) + verticalLinesStart += headerRow->height; + + // Draw vertical lines + x = getDataColumnsStartX(); + painter->drawLine(left, top, left, top + totalRowsHeight); + if (printRowNum) + painter->drawLine(x, verticalLinesStart, x, top + totalRowsHeight); + + for (int col = columnStart; col < columnEndBefore; col++) + { + x += calculatedDataColumnWidths[col]; + painter->drawLine(x, (col+1 == columnEndBefore) ? top : verticalLinesStart, x, top + totalRowsHeight); + } + + // Draw header rows + y = top; + if (headerRow) + flushDataHeaderRow(*headerRow, y, totalColumnsWidthWithRowId, columnStart, columnEndBefore); + + if (columnsHeaderRow) + flushDataHeaderRow(*columnsHeaderRow, y, totalColumnsWidthWithRowId, columnStart, columnEndBefore); + + // Draw data + int localRowNum = rowNum; + for (int rowCounter = 0; rowCounter < rowsToRender && !bufferedDataRows.isEmpty(); rowCounter++) + flushDataRow(bufferedDataRows[rowCounter], y, columnStart, columnEndBefore, localRowNum++); + + lastRowY = y; +} + +void PdfExport::flushDataRow(const DataRow& row, int& y, int columnStart, int columnEndBefore, int localRowNum) +{ + int textWidth = 0; + int textHeight = 0; + int colWidth = 0; + int x = getContentsLeft(); + + y += padding; + if (printRowNum) + { + QTextOption opt = *textOption; + opt.setAlignment(Qt::AlignRight); + + x += padding; + textWidth = rowNumColumnWidth - padding * 2; + textHeight = row.height - padding * 2; + flushDataCell(QRect(x, y, textWidth, textHeight), QString::number(localRowNum), &opt); + x += rowNumColumnWidth - padding; + } + + for (int col = columnStart; col < columnEndBefore; col++) + { + const DataCell& cell = row.cells[col]; + colWidth = calculatedDataColumnWidths[col]; + + x += padding; + textWidth = colWidth - padding * 2; + textHeight = row.height - padding * 2; + flushDataCell(QRect(x, y, textWidth, textHeight), cell); + x += colWidth - padding; + } + y += row.height - padding; +} + +void PdfExport::flushDataCell(const QRect& rect, const PdfExport::DataCell& cell) +{ + QTextOption opt = *textOption; + opt.setAlignment(cell.alignment); + + painter->save(); + if (cell.isNull) + { + painter->setPen(cfg.PdfExport.NullValueColor.get()); + painter->setFont(italicFont); + } + + painter->drawText(rect, cell.contents.left(cellDataLimit), opt); + painter->restore(); +} + +void PdfExport::flushDataCell(const QRect& rect, const QString& contents, QTextOption* opt) +{ + painter->drawText(rect, contents.left(cellDataLimit), *opt); +} + +void PdfExport::flushDataHeaderRow(const PdfExport::DataRow& row, int& y, int totalColsWidth, int columnStart, int columnEndBefore) +{ + QTextOption opt = *textOption; + opt.setAlignment(Qt::AlignHCenter); + int x = getContentsLeft(); + y += padding; + switch (row.type) + { + case DataRow::Type::TOP_HEADER: + { + x += padding; + painter->save(); + painter->setFont(boldFont); + painter->drawText(QRect(x, y, totalColsWidth - 2 * padding, row.height - 2 * padding), row.cells.first().contents, opt); + painter->restore(); + break; + } + case DataRow::Type::COLUMNS_HEADER: + { + if (printRowNum) + { + x += padding; + int textWidth = rowNumColumnWidth - padding * 2; + int textHeight = row.height - padding * 2; + painter->drawText(QRect(x, y, textWidth, textHeight), "#", opt); + x += rowNumColumnWidth - padding; + } + + for (int col = columnStart; col < columnEndBefore; col++) + flushDataHeaderCell(x, y, row, col, &opt); + + break; + } + case DataRow::Type::NORMAL: + break; // no-op + } + y += row.height - padding; +} + +void PdfExport::flushDataHeaderCell(int& x, int y, const PdfExport::DataRow& row, int col, QTextOption* opt) +{ + x += padding; + painter->drawText(QRect(x, y, calculatedDataColumnWidths[col] - 2 * padding, row.height - 2 * padding), row.cells[col].contents, *opt); + x += calculatedDataColumnWidths[col] - padding; +} + +void PdfExport::renderPageNumber() +{ + if (!printPageNumbers) + return; + + QString page = QString::number(currentPage + 1); + + QTextOption opt = *textOption; + opt.setWrapMode(QTextOption::NoWrap); + + painter->save(); + painter->setFont(italicFont); + QRect rect = painter->boundingRect(QRect(0, 0, 1, 1), page, opt).toRect(); + int x = getContentsRight() - rect.width(); + int y = getContentsBottom(); // the bottom margin was already increased to hold page numbers + QRect newRect(x, y, rect.width(), rect.height()); + painter->drawText(newRect, page, *textOption); + painter->restore(); +} + +int PdfExport::getPageNumberHeight() +{ + QTextOption opt = *textOption; + opt.setWrapMode(QTextOption::NoWrap); + + painter->save(); + painter->setFont(italicFont); + int height = painter->boundingRect(QRect(0, 0, 1, 1), "0123456789", opt).height(); + painter->restore(); + return height; +} + +void PdfExport::exportDataHeader(const QString& contents) +{ + DataRow* row = new DataRow; + row->type = DataRow::Type::TOP_HEADER; + + DataCell cell; + cell.contents = contents; + cell.alignment = Qt::AlignHCenter; + row->cells << cell; + + headerRow.reset(row); +} + +void PdfExport::exportDataColumnsHeader(const QStringList& columns) +{ + DataRow* row = new DataRow; + row->type = DataRow::Type::COLUMNS_HEADER; + + DataCell cell; + cell.alignment = Qt::AlignHCenter; + for (const QString& col : columns) + { + cell.contents = col; + row->cells << cell; + } + + columnsHeaderRow.reset(row); +} + +void PdfExport::newPage() +{ + if (currentPage < 0) + { + currentPage = 0; + renderPageNumber(); + return; + } + + pagedWriter->newPage(); + currentPage++; + lastRowY = getContentsTop(); + renderPageNumber(); +} + +void PdfExport::calculateDataColumnWidths(const QStringList& columnNames, const QList& columnDataLengths, int columnToExpand) +{ + static const QString tplChar = QStringLiteral("W"); + + // Text options for calculating widths will not allow word wrapping + QTextOption opt = *textOption; + opt.setWrapMode(QTextOption::NoWrap); + + // Calculate header width first + if (columnToExpand > -1) + { + // If any column was picked for expanding table to page width, the header will also be full page width. + // This will also result later with expanding selected column to the header width = page width. + currentHeaderMinWidth = pageWidth; + } + else + { + currentHeaderMinWidth = 0; + if (headerRow) + { + painter->save(); + painter->setFont(boldFont); + currentHeaderMinWidth = painter->boundingRect(QRectF(0, 0, 1, 1), headerRow->cells.first().contents, opt).width(); + currentHeaderMinWidth += padding * 2; + painter->restore(); + } + } + + // Calculate width of rowNum column (if enabled) + rowNumColumnWidth = 0; + if (printRowNum) + rowNumColumnWidth = painter->boundingRect(QRectF(0, 0, 1, 1), QString::number(totalRows), opt).width() + 2 * padding; + + // Precalculate column widths for the header row + QList headerWidths; + for (const QString& colName : columnNames) + headerWidths << painter->boundingRect(QRectF(0, 0, 1, 1), colName, opt).width(); + + // Calculate width for each column and compare it with its header width, then pick the wider, but never wider than the maximum width. + calculatedDataColumnWidths.clear(); + int dataWidth = 0; + int headerWidth = 0; + int totalWidth = 0; + for (int i = 0, total = columnDataLengths.size(); i < total; ++i) + { + dataWidth = painter->boundingRect(QRectF(0, 0, 1, 1), tplChar.repeated(columnDataLengths[i]), opt).width(); + headerWidth = headerWidths[i]; + + // Pick the wider one, but never wider than maxColWidth + totalWidth = qMax(dataWidth, headerWidth) + padding * 2; // wider one + padding on sides + calculatedDataColumnWidths << qMin(maxColWidth, totalWidth); + } + + // Calculate how many columns will fit for every page, until the full row is rendered. + columnsPerPage.clear(); + int colsForThePage = 0; + int currentTotalWidth = 0; + int expandColumnIndex = 0; + int dataColumnsWidth = getDataColumnsWidth(); + for (int i = 0, total = calculatedDataColumnWidths.size(); i < total; ++i) + { + colsForThePage++; + currentTotalWidth += calculatedDataColumnWidths[i]; + if (currentTotalWidth > dataColumnsWidth) + { + colsForThePage--; + columnsPerPage << colsForThePage; + + // Make sure that columns on previous page are at least as wide as the header + currentTotalWidth -= calculatedDataColumnWidths[i]; + if ((currentTotalWidth + rowNumColumnWidth) < currentHeaderMinWidth && i > 0) + { + expandColumnIndex = 1; + if (columnToExpand > -1) + expandColumnIndex = colsForThePage - columnToExpand; + + calculatedDataColumnWidths[i - expandColumnIndex] += (currentHeaderMinWidth - (currentTotalWidth + rowNumColumnWidth)); + } + + // Reset values fot next interation + currentTotalWidth = calculatedDataColumnWidths[i]; + colsForThePage = 1; + } + } + + if (colsForThePage > 0) + { + columnsPerPage << colsForThePage; + if ((currentTotalWidth + rowNumColumnWidth) < currentHeaderMinWidth && !calculatedDataColumnWidths.isEmpty()) + { + int i = calculatedDataColumnWidths.size(); + expandColumnIndex = 1; + if (columnToExpand > -1) + expandColumnIndex = colsForThePage - columnToExpand; + + calculatedDataColumnWidths[i - expandColumnIndex] += (currentHeaderMinWidth - (currentTotalWidth + rowNumColumnWidth)); + } + } +} + +void PdfExport::calculateDataRowHeights() +{ + // Calculating heights for data rows + int thisRowMaxHeight = 0; + int actualColHeight = 0; + for (DataRow& row : bufferedDataRows) + { + if (row.height > 0) // was calculated in the previous rendering iteration + continue; + + thisRowMaxHeight = 0; + for (int col = 0, total = row.cells.size(); col < total; ++col) + { + // We pass rect, that is as wide as calculated column width and we look how height extends + actualColHeight = calculateRowHeight(calculatedDataColumnWidths[col], row.cells[col].contents); + thisRowMaxHeight = qMax(thisRowMaxHeight, actualColHeight); + } + row.height = qMin(maxRowHeight, thisRowMaxHeight); + } + + // Calculating heights for header rows + totalHeaderRowsHeight = 0; + if (headerRow) + { + painter->save(); + painter->setFont(boldFont); + // Main header can be as wide as page, so that's the rect we pass + actualColHeight = calculateRowHeight(pageWidth, headerRow->cells.first().contents); + + headerRow->height = qMin(maxRowHeight, actualColHeight); + totalHeaderRowsHeight += headerRow->height; + painter->restore(); + } + + if (columnsHeaderRow) + { + thisRowMaxHeight = 0; + for (int col = 0, total = columnsHeaderRow->cells.size(); col < total; ++col) + { + // This is the same as for data rows (see above) + actualColHeight = calculateRowHeight(calculatedDataColumnWidths[col], columnsHeaderRow->cells[col].contents); + thisRowMaxHeight = qMax(thisRowMaxHeight, actualColHeight); + } + + columnsHeaderRow->height = qMin(maxRowHeight, thisRowMaxHeight); + totalHeaderRowsHeight += columnsHeaderRow->height; + } + +} + +int PdfExport::calculateRowHeight(int maxTextWidth, const QString& contents) +{ + // Measures height expanding due to constrained text width, line wrapping and top+bottom padding + return painter->boundingRect(QRect(0, 0, (maxTextWidth - padding * 2), 1), contents, *textOption).height() + padding * 2; +} + +int PdfExport::getDataColumnsWidth() const +{ + if (printRowNum) + return pageWidth - rowNumColumnWidth; + + return pageWidth; +} + +int PdfExport::getDataColumnsStartX() const +{ + if (printRowNum) + return getContentsLeft() + rowNumColumnWidth; + + return getContentsLeft(); +} + +int PdfExport::getContentsLeft() const +{ + return leftMargin; +} + +int PdfExport::getContentsTop() const +{ + return topMargin; +} + +int PdfExport::getContentsRight() const +{ + return getContentsLeft() + pageWidth; +} + +int PdfExport::getContentsBottom() const +{ + return topMargin + pageHeight; +} + +qreal PdfExport::mmToPoints(qreal sizeMM) +{ + return pointsPerMm * sizeMM; +} + +CfgMain* PdfExport::getConfig() +{ + return &cfg; +} + +QString PdfExport::getExportConfigFormName() const +{ + return "PdfExportConfig"; +} + +QFont Cfg::getPdfExportDefaultFont() +{ + QPainter p; + return p.font(); +} + +QStringList Cfg::getPdfPageSizes() +{ + return getAllPageSizes(); +} -- cgit v1.2.3