view src/converter.cpp @ 23:927794e3cc52

Add HTML output and some pdf support PDF does not look well as the CSS used for HTML is not supported.
author Andre Heinecke <andre.heinecke@intevation.de>
date Mon, 11 Apr 2016 11:00:40 +0200
parents 0b66b10a287d
children e5c5ebfa4205
line wrap: on
line source
/* Copyright (C) 2016 by ETH Zürich
 * Software engineering by Intevation GmbH
 *
 * This file is Free Software under the GNU GPL (v>=2)
 * and comes with ABSOLUTELY NO WARRANTY!
 * See LICENSE.txt for details.
 */

#include "converter.h"
#include <QDebug>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QTextDocument>
#include <QPrinter>

#include "xlsxdocument.h"
#include "xlsxconditionalformatting.h"

#include "constants.h"

QTXLSX_USE_NAMESPACE

Converter::Converter(const QString &input, const QString &output,
                     ConvertFormat fmt, const QString &title):
    QThread(Q_NULLPTR),
    mInput(input),
    mOutput(output),
    mFmt(fmt),
    mTitle(title)
{
    mTitleFmt.setFontUnderline(Format::FontUnderlineSingle);
    mTitleFmt.setFontSize(18);
    mTitleFmt.setFontName("Calibri");
    mTitleFmt.setFontBold(true);
    mTitleFmt.setVerticalAlignment(Format::AlignTop);

    mTitleStyle = QStringLiteral("style='vertical-align: top;font-weight: bold;font-size:18; text-decoration: underline;'");

    mQuestionFmt.setFontSize(11);
    mQuestionFmt.setFontName("Calibri");
    mQuestionFmt.setFontBold(true);
    mQuestionFmt.setTopBorderStyle(Format::BorderThin);
    mQuestionFmt.setBottomBorderStyle(Format::BorderThin);
    mQuestionFmt.setTextWarp(true);
    mQuestionStyle = QStringLiteral("style='border-bottom: 1pt solid black;border-top: 1pt solid black;font-weight: bold;font-size:11;'");

    mAnswerChoiceFmt.setFontSize(11);
    mAnswerChoiceFmt.setFontName("Calibri");
    mAnswerChoiceFmt.setHorizontalAlignment(Format::AlignLeft);
    mAnswerChoiceFmt.setTextWarp(true);
    mAnswerChoiceStyle= QStringLiteral("style='text-align: left;font-size:11;'");

    mChoiceTextFmt = mAnswerChoiceFmt;
    mChoiceTextFmt.setVerticalAlignment(Format::AlignVCenter);
    mChoiceTextStyle= QStringLiteral("style='text-align: right;vertical-algin: center; font-size:11;'");

    mChoiceVotesFmt = mChoiceTextFmt;
    mChoiceVotesFmt.setFontSize(10);
    mChoiceVotesStyle = QStringLiteral("style='text-align: left;vertical-algin: center; font-size:10;'");

    mFreeTextFmt = mQuestionFmt;
    mFreeTextFmt.setFontBold(false);
    mFreeTextStyle = QStringLiteral("style='border-bottom: 1pt solid black;border-top: 1pt solid black;font-size:11;'");

    mAnswerTextFmt = mQuestionFmt;
    mAnswerTextFmt.setVerticalAlignment(Format::AlignVCenter);
    mAnswerTextFmt.setHorizontalAlignment(Format::AlignLeft);
    mAnswerTextStyle = QStringLiteral("style='text-align: left;font-weight: bold;vertical-algin: center; border-bottom: 1pt solid black;border-top: 1pt solid black;font-size:11;'");
}

void Converter::run()
{
    QFile infile;

    if (mInput.isEmpty()) {
        if (!infile.open(stdin, QIODevice::ReadOnly)) {
            mErrors << tr("Failed to open standard input and no input file provided.");
            return;
        }
    } else {
        infile.setFileName(mInput);
        if (!infile.open(QIODevice::ReadOnly)) {
            mErrors << tr("Failed to open %1 for reading.").arg(mInput);
            return;
        }
    }
    QTextStream instream(&infile);

    QFile outfile;
    if (mOutput.isEmpty()) {
        if (!outfile.open(stdout, QIODevice::WriteOnly)) {
            mErrors << tr("Failed to open standard output and no output file provided.");
            return;
        }
    } else {
        outfile.setFileName(mOutput);
        if (!outfile.open(QIODevice::WriteOnly)) {
            mErrors << tr("Failed to open %1 for writing.").arg(mOutput);
            return;
        }
    }
    convertToXSLX(instream, outfile);
}

void Converter::convertToXSLX(QTextStream& instream, QFile &output)
{
    Document xlsx;
    QTextDocument doc;
    QString htmlString;
    QTextStream html (&htmlString);

    ConditionalFormatting bars;

    bars.addDataBarRule(QColor(0xFF, 0x99, 0x33), ConditionalFormatting::VOT_Num,
                        "0", ConditionalFormatting::VOT_Num, "100", false);

    const double colWidth[] = COLUMN_WIDTHS;
    double sum = 0;

    for (int i = 1; i <= COLUMN_CNT; i++) {
        xlsx.setColumnWidth(i, colWidth[i-1]);
        sum += colWidth[i-1];
    }

    double xlsx2htmlFactor = HTML_WIDTH / sum;
    int col1Width = colWidth[0] * xlsx2htmlFactor;
    int col2Width = colWidth[1] * xlsx2htmlFactor;
    int col3Width = colWidth[2] * xlsx2htmlFactor;

    /* For the merged cell wordwrap trick. */
    xlsx.setColumnWidth(26, sum + 1);
    xlsx.setColumnHidden(26, true);

    html << "<html><meta http-equiv=\"Content-Type\" content=\"text/html;charset=UTF-8\">"
            "<body><table border=\"0\" style='width:\"" << HTML_WIDTH << "px\";border-collapse:collapse'>";

    int row = 1;
    html << "<tr><th width=\"" << col1Width << "\"</th>";
    html << "<th width=\"" << col2Width << "\"</th>";
    html << "<th width=\"" << col3Width << "\"</th>";
    html << "<tr><td colspan='3' " << mTitleStyle << "/>";
    const QString title = mTitle.isEmpty() ? DEFAULT_TITLE : mTitle;
    // Set the title of the Questionaire
    xlsx.write(row++, 1, title, mTitleFmt);
    html << title.toHtmlEscaped() << "</td></tr>";
    xlsx.mergeCells("A1:C1");
    xlsx.setRowHeight(1, TITLE_ROW_HEIGHT);

    const QString input = instream.readAll();

    QRegularExpression questionEx(QUESTION_PATTERN);
    QRegularExpression choiceEx(CHOICE_PATTERN);
    QRegularExpression freetxtEx (FREETXT_PATTERN);

    QRegularExpressionMatch match = questionEx.match(input);
    bool foundSomething = false;
    int cursor = match.capturedEnd();
    while (match.hasMatch() && cursor != -1) {
        /* We've matched a question pattern. With the answer
           line */
        if (!match.lastCapturedIndex() == 2) {
            /* Should not happen without misconfiguration. */
            mErrors << "Internal parser error.";
            return;
        }
        foundSomething = true;
        const QString question = match.captured(1).trimmed();
        const QString answerLine = match.captured(2).trimmed();
        xlsx.write(row, 2, QString(" "), mQuestionFmt);
        xlsx.write(row, 3, QString(" "), mQuestionFmt);
        xlsx.write(row++, 1, question, mQuestionFmt);
        html << "<tr><td " << mQuestionStyle << ">" << question.toHtmlEscaped() << "</td>"
            "<td " << mQuestionStyle << "/>"
            "<td " << mQuestionStyle << "/></tr>";

        if (answerLine == QStringLiteral(CHOICE_IDENTIFIER)) {
            QRegularExpressionMatch choiceMatch = choiceEx.match(input, cursor);
            xlsx.setRowHeight(row, CHOICE_ROW_HEIGHT);
            xlsx.write(row++, 1, "Answer", mAnswerChoiceFmt);
            html << "<tr><td " << mAnswerChoiceStyle << ">" << "Answer" << "</td><td/><td/></tr>";
            int firstChoiceRow = row;
            int lastChoiceRow = row;
            while (choiceMatch.hasMatch() && choiceMatch.capturedStart() == cursor + 1) {
                /* We use the cursor here to keep track of the state. Only if an answer
                   follows immediately behind the last answer we treat it as valid as
                   otherwise we can't figure out when the next question begins. */
                cursor = choiceMatch.capturedEnd();

                /* Write the values */
                QString choiceName = choiceMatch.captured(1).trimmed();
                if (choiceName.startsWith("=")) {
                    choiceName = " " + choiceName;
                }
                xlsx.write(row, 1, choiceName, mChoiceTextFmt);
                html << "<tr><td " << mChoiceTextStyle << ">" << choiceName.toHtmlEscaped() << "</td>";
                bool ok;
                double percent = choiceMatch.captured(3).toDouble(&ok);
                if (!ok) {
                    mErrors << "Unparsable number in string: " + choiceMatch.captured();
                }
                html << QStringLiteral("<td style='background:linear-gradient(to right,"
                        "#ff9933, #ff9933 %1%, #ffffff %1%)'>").arg(percent);
                html << "</td>";
                xlsx.write(row, 2, percent);
                const QString numVotesString = QString("%1% | %2 Number of votes").
                           arg(choiceMatch.captured(3)).arg(choiceMatch.captured(2));
                html << "<td " << mChoiceVotesStyle << ">" << numVotesString.toHtmlEscaped() << "</td></tr>";
                xlsx.write(row, 3, numVotesString, mChoiceVotesFmt);
                xlsx.setRowHeight(row, CHOICE_ROW_HEIGHT);
                /* As long as we can match a choice which is either before the next question
                   or before the end of the document */
                choiceMatch = choiceEx.match(input, cursor);
                row++;
                lastChoiceRow++;
            }
            bars.addRange(QString("B%1:B%2").arg(firstChoiceRow).arg(lastChoiceRow));
//            xlsx.groupRows(firstChoiceRow - 2, lastChoiceRow - 1, false);
        } else if (answerLine == QStringLiteral(TEXT_IDENTIFIER)) {
            QRegularExpressionMatch textMatch = freetxtEx.match(input, cursor);
            xlsx.setRowHeight(row, CHOICE_ROW_HEIGHT);
            xlsx.write(row++, 1, "Answer", mAnswerTextFmt);
            html << "<tr><td " << mAnswerTextStyle << ">" << "Answer" << "</td><td/><td/></tr>";

            /* To handle the workaround for quotes in answers we store
             * the number of rows and only afterwards create the html rows. */
            int firstFreeRow = row;
            while (textMatch.hasMatch()) {
                if (textMatch.capturedStart() != cursor + 1) {
                    /* The format allows unescaped quotes in the text.
                       This makes a workaround neccessary. If we have
                       an Unquoted string between the next quoted string
                       and that Unquoted string is before the next question
                       we append the unquoted string and the next quoted string
                       with Quotes in the Row.*/
                    QRegularExpressionMatch nextQuestion = questionEx.match(input, cursor);
                    if (nextQuestion.hasMatch() &&
                        nextQuestion.capturedStart() < textMatch.capturedEnd()) {
                        /* The next question comes before the textMatch so we really have
                           a new question. */
                        break;
                    }
                    const QString lastRow = xlsx.read(row - 1, 26).toString();
                    int unquotedLen = textMatch.capturedStart() - cursor;
                    const QString unquoted = input.mid(cursor, unquotedLen);
                    qDebug() << "Found inner quoted string: " << unquoted;
                    /* Now combine */
                    const QString combined = QString("%1\"%2\"%3").arg(lastRow).
                                                                   arg(unquoted).
                                                                   arg(textMatch.captured(1).trimmed());
                    qDebug() << "Last row: " << lastRow;
                    qDebug() << "Next Question is at: " << nextQuestion.capturedStart();
                    qDebug() << "Text match is: " << textMatch.captured(1).trimmed();
                    qDebug() << "cursor is at: " << cursor;
                    qDebug() << "text match starts at: " << textMatch.capturedStart();
                    xlsx.write(row - 1, 26, combined, mFreeTextFmt);
                    xlsx.write(row - 1, 1, combined, mFreeTextFmt);
                    cursor = textMatch.capturedEnd();
                    textMatch = freetxtEx.match(input, cursor);
                    continue;
                }
                cursor = textMatch.capturedEnd();

                QString text = textMatch.captured(1).trimmed();
                qDebug() << "Found free text: " << text;
                if (text.startsWith("=")) {
                    text = " " + text;
                }

                /* Merge the cells */
                xlsx.mergeCells(QString("A%1:C%1").arg(row), mFreeTextFmt);

                /* Merged cells ignore wordwrap the following trick is based on:
                   http://excel.tips.net/T003207_Automatic_Row_Height_For_Merged_Cells_with_Text_Wrap.html
                */
                /* Write the values */
                xlsx.write(QString("Z%1").arg(row), text, mFreeTextFmt);
                xlsx.write(row, 1, text, mFreeTextFmt);
                row++;

                textMatch = freetxtEx.match(input, cursor);
            }
            for (int i = firstFreeRow; i < row; i++) {
                html << "<tr><td colspan='3' " << mFreeTextStyle << ">";
                html << xlsx.read(i, 1).toString().toHtmlEscaped();
                html << "</td></tr>";
            }
        }
        /* Insert Empty row. */
        xlsx.setRowHeight(row++, CHOICE_ROW_HEIGHT);
        match = questionEx.match(input, cursor);
        cursor = match.capturedEnd();
        html << QStringLiteral("<tr style='height: %1px'/>").arg(CHOICE_ROW_HEIGHT);
    }
    xlsx.addConditionalFormatting(bars);

    if (!foundSomething) {
        mErrors << tr("Failed to parse input document.");
    }

    if (mFmt == Format_XLSX && !xlsx.saveAs(&output)) {
        mErrors << tr("Saving the XLSX document failed.");
        return;
    }

    html << "</table></body></html>";

    if (mFmt == Format_HTML) {
        QTextStream outstream(&output);
        outstream << htmlString;
        return;
    }

    if (mFmt == Format_PDF) {
        output.close();
        QPrinter printer(QPrinter::PrinterResolution);
        printer.setOutputFormat(QPrinter::PdfFormat);
        printer.setPaperSize(QPrinter::A4);
        printer.setOutputFileName(output.fileName());
        doc.setHtml(htmlString);
        doc.print(&printer);
    }
}
This site is hosted by Intevation GmbH (Datenschutzerklärung und Impressum | Privacy Policy and Imprint)