andre@3: /* Copyright (C) 2016 by ETH Zürich andre@3: * Software engineering by Intevation GmbH andre@3: * andre@3: * This file is Free Software under the GNU GPL (v>=2) andre@3: * and comes with ABSOLUTELY NO WARRANTY! andre@3: * See LICENSE.txt for details. andre@3: */ andre@3: andre@3: #include "converter.h" andre@3: #include andre@3: #include andre@3: #include andre@3: andre@3: #include "xlsxdocument.h" andre@3: #include "xlsxconditionalformatting.h" andre@3: andre@3: #include "constants.h" andre@3: andre@3: QTXLSX_USE_NAMESPACE andre@3: andre@3: Converter::Converter(const QString &input, const QString &output, andre@3: ConvertFormat fmt, const QString &title): andre@3: QThread(Q_NULLPTR), andre@3: mInput(input), andre@3: mOutput(output), andre@3: mFmt(fmt), andre@3: mTitle(title) andre@3: { andre@3: mTitleFmt.setFontUnderline(Format::FontUnderlineSingle); andre@3: mTitleFmt.setFontSize(18); andre@3: mTitleFmt.setFontName("Calibri"); andre@3: mTitleFmt.setFontBold(true); andre@3: mTitleFmt.setVerticalAlignment(Format::AlignTop); andre@3: andre@3: mQuestionFmt.setFontSize(11); andre@3: mQuestionFmt.setFontName("Calibri"); andre@3: mQuestionFmt.setFontBold(true); andre@3: mQuestionFmt.setTopBorderStyle(Format::BorderThin); andre@3: mQuestionFmt.setTextWarp(true); andre@3: andre@3: mAnswerChoiceFmt.setFontSize(11); andre@3: mAnswerChoiceFmt.setFontName("Calibri"); andre@3: mAnswerChoiceFmt.setHorizontalAlignment(Format::AlignRight); andre@3: andre@3: mChoiceTextFmt = mAnswerChoiceFmt; andre@3: mChoiceTextFmt.setVerticalAlignment(Format::AlignVCenter); andre@3: andre@3: mChoiceBarFmt = mChoiceTextFmt; andre@3: mChoiceBarFmt.setFontName("Webdings"); andre@3: mChoiceBarFmt.setFontSize(9); andre@3: mChoiceBarFmt.setFontColor(QColor(0xFF, 0x99, 0x33)); andre@3: andre@3: mChoiceBarInactiveFmt = mChoiceBarFmt; andre@3: mChoiceBarInactiveFmt.setFontColor(QColor(0xD9, 0xD9, 0xD9)); andre@3: } andre@3: andre@3: void Converter::run() andre@3: { andre@3: QFile infile; andre@3: andre@3: if (mInput.isEmpty()) { andre@3: if (!infile.open(stdin, QIODevice::ReadOnly)) { andre@3: mErrors << tr("Failed to open standard input and no input file provided."); andre@3: return; andre@3: } andre@3: } else { andre@3: infile.setFileName(mInput); andre@3: if (!infile.open(QIODevice::ReadOnly)) { andre@3: mErrors << tr("Failed to open %1 for reading.").arg(mInput); andre@3: return; andre@3: } andre@3: } andre@3: QTextStream instream(&infile); andre@3: andre@3: QFile outfile; andre@3: if (mOutput.isEmpty()) { andre@3: if (!outfile.open(stdout, QIODevice::WriteOnly)) { andre@3: mErrors << tr("Failed to open standard output and no output file provided."); andre@3: return; andre@3: } andre@3: } else { andre@3: outfile.setFileName(mOutput); andre@3: if (!outfile.open(QIODevice::WriteOnly)) { andre@3: mErrors << tr("Failed to open %1 for writing.").arg(mOutput); andre@3: return; andre@3: } andre@3: } andre@3: convertToXSLX(instream, outfile); andre@3: } andre@3: andre@3: void Converter::convertToXSLX(QTextStream& instream, QFile &output) andre@3: { andre@3: Document xlsx; andre@3: andre@3: ConditionalFormatting bars; andre@3: andre@3: bars.addDataBarRule(QColor(0xFF, 0x99, 0x33), ConditionalFormatting::VOT_Num, "0", ConditionalFormatting::VOT_Num, "100", false); andre@3: andre@3: const double colWidth[] = COLUMN_WIDTHS; andre@3: for (int i = 1; i <= COLUMN_CNT; i++) { andre@3: xlsx.setColumnWidth(i, colWidth[i-1]); andre@3: } andre@3: andre@3: int row = 1; andre@3: if (!mTitle.isEmpty()) { andre@3: // Set the title of the Questionaire andre@3: xlsx.write(row++, 1, mTitle, mTitleFmt); andre@3: xlsx.mergeCells("A1:C1"); andre@3: xlsx.setRowHeight(1, TITLE_ROW_HEIGHT); andre@3: } andre@3: andre@3: const QString input = instream.readAll(); andre@3: andre@3: QRegularExpression questionEx(QUESTION_PATTERN); andre@3: QRegularExpression choiceEx(CHOICE_PATTERN); andre@3: QRegularExpression freetxtEx (FREETXT_PATTERN); andre@3: andre@3: QRegularExpressionMatch match = questionEx.match(input); andre@3: bool foundSomething = false; andre@3: int cursor = match.capturedEnd(); andre@3: while (match.hasMatch() && cursor != -1) { andre@3: /* We've matched a question pattern. With the answer andre@3: line */ andre@3: if (!match.lastCapturedIndex() == 2) { andre@3: /* Should not happen without misconfiguration. */ andre@3: mErrors << "Internal parser error."; andre@3: return; andre@3: } andre@3: foundSomething = true; andre@3: const QString question = match.captured(1).trimmed(); andre@3: const QString answerLine = match.captured(2).trimmed(); andre@3: xlsx.write(row++, 1, question, mQuestionFmt); andre@3: andre@3: if (answerLine == QStringLiteral(CHOICE_IDENTIFIER)) { andre@3: QRegularExpressionMatch choiceMatch = choiceEx.match(input, cursor); andre@3: xlsx.setRowHeight(row, CHOICE_ROW_HEIGHT); andre@3: xlsx.write(row++, 1, tr("Answer"), mAnswerChoiceFmt); andre@3: int firstChoiceRow = row; andre@3: int lastChoiceRow = row; andre@3: while (choiceMatch.hasMatch() && choiceMatch.capturedStart() == cursor + 1) { andre@3: /* We use the cursor here to keep track of the state. Only if an answer andre@3: follows immediately behind the last answer we treat it as valid as andre@3: otherwise we can't figure out when the next question begins. */ andre@3: cursor = choiceMatch.capturedEnd(); andre@3: andre@3: /* Write the values */ andre@3: xlsx.write(row, 1, choiceMatch.captured(1), mChoiceTextFmt); andre@3: bool ok; andre@3: double percent = choiceMatch.captured(3).toDouble(&ok); andre@3: if (!ok) { andre@3: mErrors << "Unparsable number in string: " + choiceMatch.captured(); andre@3: } andre@3: xlsx.write(row, 2, percent); andre@3: xlsx.write(row, 3, tr("%1% | %2 Number of votes"). andre@3: arg(choiceMatch.captured(3)).arg(choiceMatch.captured(2)), andre@3: mChoiceVotesFmt); andre@3: xlsx.setRowHeight(row, CHOICE_ROW_HEIGHT); andre@3: /* As long as we can match a choice which is either before the next question andre@3: or before the end of the document */ andre@3: choiceMatch = choiceEx.match(input, cursor); andre@3: row++; andre@3: lastChoiceRow++; andre@3: } andre@3: bars.addRange(QString("B%1:B%2").arg(firstChoiceRow).arg(lastChoiceRow)); andre@3: } else if (answerLine == QStringLiteral(TEXT_IDENTIFIER)) { andre@3: andre@3: } andre@3: /* Insert Empty row. */ andre@3: xlsx.setRowHeight(row++, CHOICE_ROW_HEIGHT); andre@3: match = questionEx.match(input, cursor); andre@3: cursor = match.capturedEnd(); andre@3: } andre@3: xlsx.addConditionalFormatting(bars); andre@3: andre@3: if (!foundSomething) { andre@3: mErrors << tr("Failed to parse input document."); andre@3: } andre@3: andre@3: if (!xlsx.saveAs(&output)) { andre@3: mErrors << tr("Saving the XLSX document failed."); andre@3: return; andre@3: } andre@3: }