|
| 1 | +/* |
| 2 | + * Copyright 2024 Kai Pastor |
| 3 | + * |
| 4 | + * This file is part of OpenOrienteering. |
| 5 | + * |
| 6 | + * OpenOrienteering is free software: you can redistribute it and/or modify |
| 7 | + * it under the terms of the GNU General Public License as published by |
| 8 | + * the Free Software Foundation, either version 3 of the License, or |
| 9 | + * (at your option) any later version. |
| 10 | + * |
| 11 | + * OpenOrienteering is distributed in the hope that it will be useful, |
| 12 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | + * GNU General Public License for more details. |
| 15 | + * |
| 16 | + * You should have received a copy of the GNU General Public License |
| 17 | + * along with OpenOrienteering. If not, see <http://www.gnu.org/licenses/>. |
| 18 | + */ |
| 19 | + |
| 20 | +#include "html_symbol_report.h" |
| 21 | + |
| 22 | +#include <QBuffer> |
| 23 | +#include <QByteArray> |
| 24 | +#include <QChar> |
| 25 | +#include <QColor> |
| 26 | +#include <QCoreApplication> |
| 27 | +#include <QImage> |
| 28 | +#include <QImageWriter> |
| 29 | +#include <QLatin1String> |
| 30 | +#include <QLocale> |
| 31 | +#include <QString> |
| 32 | +#include <QTextDocumentFragment> |
| 33 | + |
| 34 | +#include "core/map.h" |
| 35 | +#include "core/map_color.h" |
| 36 | +#include "core/symbols/symbol.h" |
| 37 | + |
| 38 | +namespace OpenOrienteering { |
| 39 | + |
| 40 | +/** |
| 41 | + * Generates HTML reports for colors and symbols in a Map. |
| 42 | + * |
| 43 | + * The public interface of this class is the free-standing function |
| 44 | + * makeHTMLSymbolReport(const Map& map). |
| 45 | + */ |
| 46 | +class HTMLSymbolReportGenerator |
| 47 | +{ |
| 48 | +private: |
| 49 | + /** |
| 50 | + * Efficiently produce PNG data from QImage. |
| 51 | + * |
| 52 | + * An object of this class holds a buffer which is reused in multiple calls |
| 53 | + * to write(). So the number of actual memory allocations can be kept small. |
| 54 | + */ |
| 55 | + class PNGImageWriter |
| 56 | + { |
| 57 | + QByteArray png {"PNG"}; |
| 58 | + QByteArray png_data; |
| 59 | + |
| 60 | + public: |
| 61 | + /** |
| 62 | + * Creates PNG data from an image. |
| 63 | + * |
| 64 | + * @return A reference to a shared buffer. The buffer is invalidated |
| 65 | + * when the function is called again (for the same writer). |
| 66 | + */ |
| 67 | + const QByteArray& write(const QImage& image) |
| 68 | + { |
| 69 | + png_data.clear(); |
| 70 | + QBuffer buffer(&png_data); |
| 71 | + QImageWriter(&buffer, png).write(image); |
| 72 | + return png_data; |
| 73 | + } |
| 74 | + }; |
| 75 | + |
| 76 | + enum class ColorRowType { |
| 77 | + Basic, ///< A color row with number, icon and name |
| 78 | + Extended ///< A color row with all details |
| 79 | + }; |
| 80 | + |
| 81 | + const Map& map; |
| 82 | + QImage icon_image; |
| 83 | + QLocale locale; |
| 84 | + PNGImageWriter image_writer; |
| 85 | + |
| 86 | + QString imgForColor(const QColor& c, const QString& alt) |
| 87 | + { |
| 88 | + icon_image.fill(c); |
| 89 | + return QString::fromLatin1("<img alt=\"%1\" src=\"data:image/png;base64,").arg(alt) |
| 90 | + + QString::fromLatin1(image_writer.write(icon_image).toBase64()) |
| 91 | + + QString::fromLatin1("\">"); |
| 92 | + } |
| 93 | + |
| 94 | + QString makeColorRow(const MapColor& c, ColorRowType row_type) |
| 95 | + { |
| 96 | + icon_image = QImage(16, 16, QImage::Format_ARGB32); |
| 97 | + |
| 98 | + auto details = QString {}; |
| 99 | + if (row_type == ColorRowType::Extended) |
| 100 | + { |
| 101 | + auto cmyk = c.getCmyk(); |
| 102 | + details = QString::fromLatin1( |
| 103 | + "<td>%1</td>" // c |
| 104 | + "<td>%2</td>" // m |
| 105 | + "<td>%3</td>" // y |
| 106 | + "<td>%4</td>" // k |
| 107 | + "<td style=\"font-family:monospace\">%5</td>" // rgb |
| 108 | + "<td>%6</td>" // spot color |
| 109 | + "<td>%7</td>" // knockout |
| 110 | + ).arg( |
| 111 | + locale.toString(100*cmyk.c, 'g', 3), |
| 112 | + locale.toString(100*cmyk.m, 'g', 3), |
| 113 | + locale.toString(100*cmyk.y, 'g', 3), |
| 114 | + locale.toString(100*cmyk.k, 'g', 3), |
| 115 | + QColor(c.getRgb()).name(), |
| 116 | + QString{c.getSpotColorName()}.replace(QString::fromLatin1(", "), QString::fromLatin1(",<br>")), |
| 117 | + c.getKnockout() ? QString::fromLatin1("[X]") : QString{} ); |
| 118 | + } |
| 119 | + |
| 120 | + auto name = QTextDocumentFragment::fromHtml(c.getName()).toPlainText(); |
| 121 | + return QString::fromLatin1( |
| 122 | + "<tr>" |
| 123 | + "<td>%1</td>" // level |
| 124 | + "<td>%2</td>" // icon |
| 125 | + "<td class=\"name\">%3</td>" // name |
| 126 | + "%9" // details |
| 127 | + "</tr>\n" |
| 128 | + ).arg( |
| 129 | + QString::number(c.getPriority()), |
| 130 | + imgForColor(c, name), |
| 131 | + name, |
| 132 | + details ); |
| 133 | + } |
| 134 | + |
| 135 | + QString makeColorSection() |
| 136 | + { |
| 137 | + QString color_data; |
| 138 | + map.applyOnAllColors([this, &color_data](const MapColor* c) { |
| 139 | + color_data.append(makeColorRow(*c, ColorRowType::Extended)); |
| 140 | + }); |
| 141 | + return QString::fromLatin1( |
| 142 | + "<h2>%1</h2>\n" |
| 143 | + "<table class=\"colors\">\n" |
| 144 | + "<thead>\n" |
| 145 | + "<tr>" |
| 146 | + "<th colspan=\"2\">%2</th>" |
| 147 | + "<th class=\"name\">%3</th>" |
| 148 | + "<th>C</th>" |
| 149 | + "<th>M</th>" |
| 150 | + "<th>Y</th>" |
| 151 | + "<th>K</th>" |
| 152 | + "<th>%4</th>" |
| 153 | + "<th>%5</th>" |
| 154 | + "<th>%6</th>" |
| 155 | + "</tr>\n" |
| 156 | + "</thead>\n" |
| 157 | + "<tbody>\n" |
| 158 | + "%9" |
| 159 | + "</tbody>\n" |
| 160 | + "</table>\n" |
| 161 | + ).arg( |
| 162 | + QCoreApplication::translate("OpenOrienteering::SymbolReport", "Map Colors"), |
| 163 | + QCoreApplication::translate("OpenOrienteering::SymbolReport", "Color"), |
| 164 | + QCoreApplication::translate("OpenOrienteering::SymbolReport", "Name"), |
| 165 | + QCoreApplication::translate("OpenOrienteering::SymbolReport", "RGB"), |
| 166 | + QCoreApplication::translate("OpenOrienteering::SymbolReport", "Spot colors"), |
| 167 | + QCoreApplication::translate("OpenOrienteering::SymbolReport", "Knockout"), |
| 168 | + color_data ); |
| 169 | + } |
| 170 | + |
| 171 | + QString imgForSymbol(const Symbol& s, const QString& alt) |
| 172 | + { |
| 173 | + // For better quality and compression, |
| 174 | + // using multiple of display size but no antialiasing |
| 175 | + auto const display_size = 48; |
| 176 | + icon_image = s.createIcon(map, 4 * display_size, false); |
| 177 | + return QString::fromLatin1("<img alt=\"%1\" width=\"%2\" src=\"data:image/png;base64,").arg(alt).arg(display_size) |
| 178 | + + QString::fromLatin1(image_writer.write(icon_image).toBase64()) |
| 179 | + + QString::fromLatin1("\">"); |
| 180 | + } |
| 181 | + |
| 182 | + QString colorsForSymbol(const Symbol& s) |
| 183 | + { |
| 184 | + QString color_data; |
| 185 | + map.applyOnAllColors([this, &s, &color_data](const MapColor* c){ |
| 186 | + if (s.containsColor(c)) |
| 187 | + color_data.append(makeColorRow(*c, ColorRowType::Basic)); |
| 188 | + }); |
| 189 | + return QString::fromLatin1( |
| 190 | + "<table>\n" |
| 191 | + "<tbody>\n" |
| 192 | + "%1" |
| 193 | + "</tbody>\n" |
| 194 | + "</table>\n" |
| 195 | + ).arg( |
| 196 | + color_data ); |
| 197 | + } |
| 198 | + |
| 199 | + QString makeSymbolRow(const Symbol& s) |
| 200 | + { |
| 201 | + auto label = QString { s.getNumberAsString() |
| 202 | + + QChar::Space |
| 203 | + + QTextDocumentFragment::fromHtml(s.getName()).toPlainText() }; |
| 204 | + auto extra_text = QString{}; |
| 205 | + if (s.isRotatable()) |
| 206 | + extra_text += QCoreApplication::translate("OpenOrienteering::SymbolReport", "[X] %1") |
| 207 | + .arg(QCoreApplication::translate("OpenOrienteering::SymbolReport", "Symbol orientation can be changed.")) |
| 208 | + + QLatin1String("<br>\n"); |
| 209 | + if (s.hasRotatableFillPattern()) |
| 210 | + extra_text += QCoreApplication::translate("OpenOrienteering::SymbolReport", "[X] %1") |
| 211 | + .arg(QCoreApplication::translate("OpenOrienteering::SymbolReport", "Pattern orientation can be changed.")) |
| 212 | + + QLatin1String("<br>\n"); |
| 213 | + if (s.isHelperSymbol()) |
| 214 | + extra_text += QCoreApplication::translate("OpenOrienteering::SymbolReport", "[X] %1") |
| 215 | + .arg(QCoreApplication::translate("OpenOrienteering::SymbolReport", "Helper symbol")) |
| 216 | + + QLatin1String("<br>\n"); |
| 217 | + return QString::fromLatin1( |
| 218 | + "<tr>" |
| 219 | + "<td style=\"vertical-align:top;\">%1</td>\n" // icon |
| 220 | + "<td style=\"vertical-align:middle;\"><b>%2</b></td>" // label |
| 221 | + "</tr>\n" |
| 222 | + "<tr>" |
| 223 | + "<td> </td>\n" |
| 224 | + "<td style=\"padding-bottom:18px;\">\n" |
| 225 | + "<div>\n%3</div>\n" // description |
| 226 | + "<p>%4</p>\n" // configuration properties |
| 227 | + "%5</td>" // colors |
| 228 | + "</tr>\n" |
| 229 | + ).arg( |
| 230 | + imgForSymbol(s, label), |
| 231 | + label, |
| 232 | + QString(s.getDescription()).replace(QChar::LineFeed, QLatin1String("<br>\n")), |
| 233 | + extra_text, |
| 234 | + colorsForSymbol(s) ); |
| 235 | + } |
| 236 | + |
| 237 | + QString makeSymbolSection() |
| 238 | + { |
| 239 | + QString symbol_data; |
| 240 | + map.applyOnAllSymbols([this, &symbol_data](const Symbol* s){ |
| 241 | + symbol_data.append(makeSymbolRow(*s)); |
| 242 | + }); |
| 243 | + return QString::fromLatin1( |
| 244 | + "<h2>%1</h2>\n" |
| 245 | + "<table class=\"symbols\">\n" |
| 246 | + "<tbody>\n" |
| 247 | + "%2" |
| 248 | + "</tbody>\n" |
| 249 | + "</table>\n" |
| 250 | + ).arg( |
| 251 | + QCoreApplication::translate("OpenOrienteering::SymbolReport", "Symbols"), |
| 252 | + symbol_data ); |
| 253 | + } |
| 254 | + |
| 255 | +public: |
| 256 | + explicit HTMLSymbolReportGenerator(const Map& map) |
| 257 | + : map(map) |
| 258 | + {} |
| 259 | + |
| 260 | + QString generate() |
| 261 | + { |
| 262 | + return QString::fromLatin1( |
| 263 | + "<!DOCTYPE html>\n" |
| 264 | + "<html>\n" |
| 265 | + "<head>\n" |
| 266 | + "<meta charset=\"utf-8\">" |
| 267 | + "<title>%0</title>\n" |
| 268 | + "<meta name=\"generator\" content=\"OpenOrienteering Mapper\">\n" |
| 269 | + "<style>\n" |
| 270 | + "th { font-size: 120%; text-align: center; }\n" |
| 271 | + "th, td { padding: 4px; }\n" |
| 272 | + "table.colors { text-align: center; }\n" |
| 273 | + "table.colors td:first-child { text-align: right; }\n" |
| 274 | + "table.colors td.name { text-align: left; }\n" |
| 275 | + "table.symbols { max-width: 60em; }\n" |
| 276 | + "</style>\n" |
| 277 | + "</head>\n" |
| 278 | + "<body>\n" |
| 279 | + "<h1>%0</h1>\n" |
| 280 | + "%1" |
| 281 | + "%2" |
| 282 | + "</body>\n" |
| 283 | + "</html>" |
| 284 | + ).arg( |
| 285 | + QCoreApplication::translate("OpenOrienteering::SymbolReport", "Symbol Set Report on '%0'").arg(map.symbolSetId()), |
| 286 | + makeColorSection(), |
| 287 | + makeSymbolSection() ); |
| 288 | + } |
| 289 | +}; |
| 290 | + |
| 291 | + |
| 292 | +QString makeHTMLSymbolReport(const Map& map) |
| 293 | +{ |
| 294 | + return HTMLSymbolReportGenerator(map).generate(); |
| 295 | +} |
| 296 | + |
| 297 | +} // namespace OpenOrienteering |
0 commit comments