1 /*
2  *    Copyright 2012, 2013 Pete Curtis
3  *    Copyright 2013-2019 Kai Pastor
4  *
5  *    This file is part of OpenOrienteering.
6  *
7  *    OpenOrienteering is free software: you can redistribute it and/or modify
8  *    it under the terms of the GNU General Public License as published by
9  *    the Free Software Foundation, either version 3 of the License, or
10  *    (at your option) any later version.
11  *
12  *    OpenOrienteering is distributed in the hope that it will be useful,
13  *    but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *    GNU General Public License for more details.
16  *
17  *    You should have received a copy of the GNU General Public License
18  *    along with OpenOrienteering.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "ocad8_file_format.h"
22 #include "ocad8_file_format_p.h"
23 
24 #include <QtMath>
25 #include <QCoreApplication>
26 #include <QDir>
27 #include <QFileInfo>
28 #include <QImage>
29 #include <QImageReader>
30 #include <QRgb>
31 #include <QTextCodec>
32 
33 #include "settings.h"
34 #include "core/georeferencing.h"
35 #include "core/map.h"
36 #include "core/map_color.h"
37 #include "core/map_coord.h"
38 #include "core/map_part.h"
39 #include "core/map_view.h"
40 #include "core/objects/object.h"
41 #include "core/objects/text_object.h"
42 #include "core/symbols/symbol.h"
43 #include "core/symbols/area_symbol.h"
44 #include "core/symbols/combined_symbol.h"
45 #include "core/symbols/line_symbol.h"
46 #include "core/symbols/point_symbol.h"
47 #include "core/symbols/text_symbol.h"
48 #include "fileformats/xml_file_format.h"
49 #include "fileformats/file_import_export.h"
50 #include "fileformats/ocd_file_format.h"
51 #include "templates/template.h"
52 #include "templates/template_image.h"
53 #include "templates/template_map.h"
54 #include "util/encoding.h"
55 #include "util/util.h"
56 
57 
58 namespace OpenOrienteering {
59 
60 // ### OCAD8FileFormat ###
61 
OCAD8FileFormat()62 OCAD8FileFormat::OCAD8FileFormat()
63  : FileFormat(MapFile,
64               "OCAD78",
65               ::OpenOrienteering::ImportExport::tr("OCAD Versions 7, 8"),
66               QString::fromLatin1("ocd"),
67               Feature::FileOpen | Feature::FileImport | Feature::ReadingLossy |
68               Feature::FileSave | Feature::FileSaveAs | Feature::WritingLossy )
69 {
70 	// Nothing
71 }
72 
73 
understands(const char * buffer,int size) const74 FileFormat::ImportSupportAssumption OCAD8FileFormat::understands(const char* buffer, int size) const
75 {
76     // The first two bytes of the file must be AD 0C.
77 	if (size < 2)
78 		return Unknown;
79 	else if (quint8(buffer[0]) == 0xAD && buffer[1] == 0x0C)
80 		return FullySupported;
81 	else
82 		return NotSupported;
83 }
84 
85 
makeImporter(const QString & path,Map * map,MapView * view) const86 std::unique_ptr<Importer> OCAD8FileFormat::makeImporter(const QString& path, Map *map, MapView *view) const
87 {
88 	return std::make_unique<OCAD8FileImport>(path, map, view);
89 }
90 
makeExporter(const QString & path,const Map * map,const MapView * view) const91 std::unique_ptr<Exporter> OCAD8FileFormat::makeExporter(const QString& path, const Map* map, const MapView* view) const
92 {
93 	return std::make_unique<OCAD8FileExport>(path, map, view);
94 }
95 
96 
97 
98 // ### OCAD8FileImport ###
99 
OCAD8FileImport(const QString & path,Map * map,MapView * view)100 OCAD8FileImport::OCAD8FileImport(const QString& path, Map* map, MapView* view) : Importer(path, map, view), file(nullptr)
101 {
102     ocad_init();
103     const QByteArray enc_name = Settings::getInstance().getSetting(Settings::General_Local8BitEncoding).toByteArray();
104     encoding_1byte = Util::codecForName(enc_name);
105     if (!encoding_1byte) encoding_1byte = QTextCodec::codecForLocale();
106     encoding_2byte = QTextCodec::codecForName("UTF-16LE");
107     offset_x = offset_y = 0;
108 }
109 
~OCAD8FileImport()110 OCAD8FileImport::~OCAD8FileImport()
111 {
112     ocad_shutdown();
113 }
114 
isRasterImageFile(const QString & filename)115 bool OCAD8FileImport::isRasterImageFile(const QString &filename)
116 {
117 	int dot_pos = filename.lastIndexOf(QLatin1Char('.'));
118 	if (dot_pos < 0)
119 		return false;
120 	QString extension = filename.right(filename.length() - dot_pos - 1).toLower();
121 	return QImageReader::supportedImageFormats().contains(extension.toLatin1());
122 }
123 
importImplementation()124 bool OCAD8FileImport::importImplementation()
125 {
126     //qint64 start = QDateTime::currentMSecsSinceEpoch();
127 
128 	auto& device = *this->device();
129 	u32 size = device.bytesAvailable();
130 	u8* buffer = (u8*)malloc(size);
131 	if (!buffer)
132 		throw FileFormatException(tr("Could not allocate buffer."));
133 	if (device.read((char*)buffer, size) != size)
134 		throw FileFormatException(device.errorString());
135 	int err = ocad_file_open_memory(&file, buffer, size);
136     if (err != 0) throw FileFormatException(tr("libocad returned %1").arg(err));
137 
138 	if (file->header->major <= 5 || file->header->major >= 9)
139 		throw FileFormatException(tr("OCAD files of version %1 are not supported!").arg(file->header->major));
140 
141     //qDebug() << "file version is" << file->header->major << ", type is"
142     //         << ((file->header->ftype == 2) ? "normal" : "other");
143     //qDebug() << "map scale is" << file->setup->scale;
144 
145 	map->setProperty(OcdFileFormat::versionProperty(), file->header->major);
146 
147 	// Scale and georeferencing parameters
148 	Georeferencing georef;
149 	georef.setScaleDenominator(file->setup->scale);
150 	georef.setProjectedRefPoint(QPointF(file->setup->offsetx, file->setup->offsety));
151 	if (qAbs(file->setup->angle) >= 0.01) /* degrees */
152 	{
153 		georef.setGrivation(file->setup->angle);
154 	}
155 	map->setGeoreferencing(georef);
156 
157 	map->setMapNotes(convertCString((const char*)file->buffer + file->header->infopos, file->header->infosize, false));
158 
159     // TODO: print parameters
160 
161 	// Load the separations to a temporary stack
162 	std::vector< MapColor* > separations;
163 	int num_separations = ocad_separation_count(file);
164 #if 1
165 	addWarning(tr("%n color separation(s) were skipped, reason: Import disabled.", "", num_separations));
166 	num_separations = 0;
167 #endif
168 	if (num_separations < 0)
169 	{
170 		addWarning(tr("Could not load the spot color definitions, error: %1").arg(num_separations));
171 		num_separations = 0;
172 	}
173 	separations.reserve(num_separations);
174 	for (int i = 0; i < num_separations; i++)
175 	{
176 		const OCADColorSeparation *ocad_separation = ocad_separation_at(file, i);
177 		MapColor* color = new MapColor(convertPascalString(ocad_separation->sep_name), MapColor::Reserved);
178 		color->setSpotColorName(convertPascalString(ocad_separation->sep_name).toUpper());
179 		// OCD stores CMYK values as integers from 0-200.
180 		const MapColorCmyk cmyk(
181 		  0.005f * ocad_separation->cyan,
182 		  0.005f * ocad_separation->magenta,
183 		  0.005f * ocad_separation->yellow,
184 		  0.005f * ocad_separation->black );
185 		color->setCmyk(cmyk);
186 		color->setOpacity(1.0f);
187 		separations.push_back(color);
188 	}
189 
190 	// Load colors
191 	int num_colors = ocad_color_count(file);
192 	for (int i = 0; i < num_colors; i++)
193 	{
194 		OCADColor *ocad_color = ocad_color_at(file, i);
195 		MapColor* color = new MapColor(convertPascalString(ocad_color->name), map->color_set->colors.size());
196 		// OCD stores CMYK values as integers from 0-200.
197 		MapColorCmyk cmyk(
198 		  0.005f * ocad_color->cyan,
199 		  0.005f * ocad_color->magenta,
200 		  0.005f * ocad_color->yellow,
201 		  0.005f * ocad_color->black );
202 		color->setCmyk(cmyk);
203 		color->setOpacity(1.0f);
204 
205 		SpotColorComponents components;
206 		for (int j = 0; j < num_separations; ++j)
207 		{
208 			const u8& ocad_halftone = ocad_color->spot[j];
209 			if (ocad_halftone <= 200)
210 			{
211 				float halftone = 0.005f * ocad_halftone;
212 				components.reserve(std::size_t(num_separations));  // reserves only once for same capacity
213 				components.push_back(SpotColorComponent(separations[j], halftone));  // clazy:exclude=reserve-candidates
214 			}
215 		}
216 		if (!components.empty())
217 		{
218 			color->setSpotColorComposition(components);
219 			const MapColorCmyk cmyk(color->getCmyk());
220 			color->setCmykFromSpotColors();
221 			if (cmyk != color->getCmyk())
222 				// The color's CMYK was customized.
223 				color->setCmyk(cmyk);
224 		}
225 
226 		if (i == 0 && color->isBlack() && color->getName() == QLatin1String("Registration black")
227 		           && XMLFileFormat::active_version >= 6 )
228 		{
229 			delete color; color = nullptr;
230 			color_index[ocad_color->number] = Map::getRegistrationColor();
231 			addWarning(tr("Color \"Registration black\" is imported as a special color."));
232 			// NOTE: This does not make a difference in output
233 			// as long as no spot colors are created,
234 			// but as a special color, it is protected from modification,
235 			// and it will be saved as number 0 in OCD export.
236 		}
237 		else
238 		{
239 			map->color_set->colors.push_back(color);
240 			color_index[ocad_color->number] = color;
241 		}
242 	}
243 
244 	// Insert the spot colors into the map
245 	for (int i = 0; i < num_separations; ++i)
246 	{
247 		map->addColor(separations[i], map->color_set->colors.size());
248 	}
249 
250     // Load symbols
251     for (OCADSymbolIndex *idx = ocad_symidx_first(file); idx; idx = ocad_symidx_next(file, idx))
252     {
253         for (int i = 0; i < 256; i++)
254         {
255             OCADSymbol *ocad_symbol = ocad_symbol_at(file, idx, i);
256             if (ocad_symbol && ocad_symbol->number != 0)
257             {
258                 Symbol *symbol = nullptr;
259                 if (ocad_symbol->type == OCAD_POINT_SYMBOL)
260                 {
261                     symbol = importPointSymbol((OCADPointSymbol *)ocad_symbol);
262                 }
263                 else if (ocad_symbol->type == OCAD_LINE_SYMBOL)
264                 {
265                     symbol = importLineSymbol((OCADLineSymbol *)ocad_symbol);
266                 }
267                 else if (ocad_symbol->type == OCAD_AREA_SYMBOL)
268                 {
269                     symbol = importAreaSymbol((OCADAreaSymbol *)ocad_symbol);
270                 }
271                 else if (ocad_symbol->type == OCAD_TEXT_SYMBOL)
272                 {
273                     symbol = importTextSymbol((OCADTextSymbol *)ocad_symbol);
274                 }
275                 else if (ocad_symbol->type == OCAD_RECT_SYMBOL)
276                 {
277 					RectangleInfo* rect = importRectSymbol((OCADRectSymbol *)ocad_symbol);
278 					map->symbols.push_back(rect->border_line);
279 					if (rect->has_grid)
280 					{
281 						map->symbols.push_back(rect->inner_line);
282 						map->symbols.push_back(rect->text);
283 					}
284 					continue;
285                 }
286 
287 
288 				if (symbol)
289 				{
290 					map->symbols.push_back(symbol);
291 					symbol_index[ocad_symbol->number] = symbol;
292                 }
293                 else
294                 {
295                     addWarning(tr("Unable to import symbol \"%3\" (%1.%2)")
296                                .arg(ocad_symbol->number / 10).arg(ocad_symbol->number % 10)
297                                .arg(convertPascalString(ocad_symbol->name)));
298                 }
299             }
300         }
301     }
302 
303     if (!loadSymbolsOnly())
304 	{
305 		// Load objects
306 
307 		// Place all objects into a single OCAD import part
308 		MapPart* part = new MapPart(tr("OCAD import layer"), map);
309 		for (OCADObjectIndex *idx = ocad_objidx_first(file); idx; idx = ocad_objidx_next(file, idx))
310 		{
311 			for (int i = 0; i < 256; i++)
312 			{
313 				OCADObjectEntry *entry = ocad_object_entry_at(file, idx, i);
314 				OCADObject *ocad_obj = ocad_object(file, entry);
315 				if (ocad_obj)
316 				{
317 					Object *object = importObject(ocad_obj, part);
318 					if (object) {
319 						part->objects.push_back(object);
320 					}
321 				}
322 			}
323 		}
324 		delete map->parts[0];
325 		map->parts[0] = part;
326 		map->current_part_index = 0;
327 
328 		// Load templates
329 		map->templates.clear();
330 		for (OCADStringIndex *idx = ocad_string_index_first(file); idx; idx = ocad_string_index_next(file, idx))
331 		{
332 			for (int i = 0; i < 256; i++)
333 			{
334 				OCADStringEntry *entry = ocad_string_entry_at(file, idx, i);
335 				if (entry->type != 0 && entry->size > 0)
336 					importString(entry);
337 			}
338 		}
339 		map->first_front_template = map->templates.size(); // Templates in front of the map are not supported by OCD
340 
341 		// Fill view with relevant fields from OCD file
342 		if (view)
343 		{
344 			if (file->setup->zoom >= MapView::zoom_out_limit && file->setup->zoom <= MapView::zoom_in_limit)
345 				view->setZoom(file->setup->zoom);
346 
347 			s32 buf[3];
348 			ocad_point(buf, &file->setup->center);
349 			MapCoord center_pos;
350 			convertPoint(center_pos, buf[0], buf[1]);
351 			view->setCenter(center_pos);
352 		}
353 
354 		// TODO: read template visibilities
355 		/*
356 			int num_template_visibilities;
357 			file->read((char*)&num_template_visibilities, sizeof(int));
358 
359 			for (int i = 0; i < num_template_visibilities; ++i)
360 			{
361 				int pos;
362 				file->read((char*)&pos, sizeof(int));
363 
364                 TemplateVisibility* vis = getTemplateVisibility(map->getTemplate(pos));
365 				file->read((char*)&vis->visible, sizeof(bool));
366 				file->read((char*)&vis->opacity, sizeof(float));
367 			}
368 		}
369 		*/
370 
371 		// Undo steps are not supported in OCAD
372     }
373 
374     ocad_file_close(file);
375 
376     //qint64 elapsed = QDateTime::currentMSecsSinceEpoch() - start;
377 	//qDebug() << "OCAD map imported:"<<map->getNumSymbols()<<"symbols and"<<map->getNumObjects()<<"objects in"<<elapsed<<"milliseconds";
378 
379 	emit map->currentMapPartIndexChanged(map->current_part_index);
380 	emit map->currentMapPartChanged(map->getPart(map->current_part_index));
381 
382 	return true;
383 }
384 
setStringEncodings(const char * narrow,const char * wide)385 void OCAD8FileImport::setStringEncodings(const char *narrow, const char *wide) {
386     encoding_1byte = QTextCodec::codecForName(narrow);
387     encoding_2byte = QTextCodec::codecForName(wide);
388 }
389 
importPointSymbol(const OCADPointSymbol * ocad_symbol)390 Symbol *OCAD8FileImport::importPointSymbol(const OCADPointSymbol *ocad_symbol)
391 {
392     PointSymbol *symbol = importPattern(ocad_symbol->ngrp, (OCADPoint *)ocad_symbol->pts);
393     fillCommonSymbolFields(symbol, (OCADSymbol *)ocad_symbol);
394 	symbol->setRotatable(ocad_symbol->base_flags & 1);
395 
396     return symbol;
397 }
398 
importLineSymbol(const OCADLineSymbol * ocad_symbol)399 Symbol *OCAD8FileImport::importLineSymbol(const OCADLineSymbol *ocad_symbol)
400 {
401 	LineSymbol* line_for_borders = nullptr;
402 
403 	// Import a main line?
404 	LineSymbol* main_line = nullptr;
405 	if (ocad_symbol->dmode == 0 || ocad_symbol->width > 0)
406 	{
407 		main_line = new LineSymbol();
408 		line_for_borders = main_line;
409 		fillCommonSymbolFields(main_line, (OCADSymbol *)ocad_symbol);
410 
411 		// Basic line options
412 		main_line->line_width = convertSize(ocad_symbol->width);
413 		main_line->color = convertColor(ocad_symbol->color);
414 
415 		// Cap and join styles
416 		if (ocad_symbol->ends == 0)
417 		{
418 			main_line->cap_style = LineSymbol::FlatCap;
419 			main_line->join_style = LineSymbol::BevelJoin;
420 		}
421 		else if (ocad_symbol->ends == 1)
422 		{
423 			main_line->cap_style = LineSymbol::RoundCap;
424 			main_line->join_style = LineSymbol::RoundJoin;
425 		}
426 		else if (ocad_symbol->ends == 2)
427 		{
428 			main_line->cap_style = LineSymbol::PointedCap;
429 			main_line->join_style = LineSymbol::BevelJoin;
430 		}
431 		else if (ocad_symbol->ends == 3)
432 		{
433 			main_line->cap_style = LineSymbol::PointedCap;
434 			main_line->join_style = LineSymbol::RoundJoin;
435 		}
436 		else if (ocad_symbol->ends == 4)
437 		{
438 			main_line->cap_style = LineSymbol::FlatCap;
439 			main_line->join_style = LineSymbol::MiterJoin;
440 		}
441 		else if (ocad_symbol->ends == 6)
442 		{
443 			main_line->cap_style = LineSymbol::PointedCap;
444 			main_line->join_style = LineSymbol::MiterJoin;
445 		}
446 
447 		main_line->start_offset = convertSize(ocad_symbol->bdist);
448 		main_line->end_offset = convertSize(ocad_symbol->edist);
449 		if (main_line->cap_style == LineSymbol::PointedCap)
450 		{
451 			// Note: While the property in the file may be different
452 			// (cf. what is set in the first place), OC*D always
453 			// draws round joins if the line cap is pointed!
454 			main_line->join_style = LineSymbol::RoundJoin;
455 		}
456 
457 		// Handle the dash pattern
458 		if( ocad_symbol->gap > 0 || ocad_symbol->gap2 > 0 )
459 		{
460 			main_line->dashed = true;
461 
462 			// Detect special case
463 			if (ocad_symbol->gap2 > 0 && ocad_symbol->gap == 0)
464 			{
465 				main_line->dash_length = convertSize(ocad_symbol->len - ocad_symbol->gap2);
466 				main_line->break_length = convertSize(ocad_symbol->gap2);
467 				if (!(ocad_symbol->elen >= ocad_symbol->len / 2 - 1 && ocad_symbol->elen <= ocad_symbol->len / 2 + 1))
468 					addWarning(tr("In dashed line symbol %1, the end length cannot be imported correctly.").arg(0.1 * ocad_symbol->number));
469 				if (ocad_symbol->egap != 0)
470 					addWarning(tr("In dashed line symbol %1, the end gap cannot be imported correctly.").arg(0.1 * ocad_symbol->number));
471 			}
472 			else
473 			{
474 				if (ocad_symbol->len != ocad_symbol->elen)
475 				{
476 					if (ocad_symbol->elen >= ocad_symbol->len / 2 - 1 && ocad_symbol->elen <= ocad_symbol->len / 2 + 1)
477 						main_line->half_outer_dashes = true;
478 					else
479 						addWarning(tr("In dashed line symbol %1, main and end length are different (%2 and %3). Using %4.")
480 						.arg(0.1 * ocad_symbol->number).arg(ocad_symbol->len).arg(ocad_symbol->elen).arg(ocad_symbol->len));
481 				}
482 
483 				main_line->dash_length = convertSize(ocad_symbol->len);
484 				main_line->break_length = convertSize(ocad_symbol->gap);
485 				if (ocad_symbol->gap2 > 0)
486 				{
487 					main_line->dashes_in_group = 2;
488 					if (ocad_symbol->gap2 != ocad_symbol->egap)
489 						addWarning(tr("In dashed line symbol %1, gaps D and E are different (%2 and %3). Using %4.")
490 						.arg(0.1 * ocad_symbol->number).arg(ocad_symbol->gap2).arg(ocad_symbol->egap).arg(ocad_symbol->gap2));
491 					main_line->in_group_break_length = convertSize(ocad_symbol->gap2);
492 					main_line->dash_length = (main_line->dash_length - main_line->in_group_break_length) / 2;
493 				}
494 			}
495 		}
496 		else
497 		{
498 			main_line->segment_length = convertSize(ocad_symbol->len);
499 			main_line->end_length = convertSize(ocad_symbol->elen);
500 		}
501 	}
502 
503 	// Import a 'framing' line?
504 	LineSymbol* framing_line = nullptr;
505 	if (ocad_symbol->fwidth > 0)
506 	{
507 		framing_line = new LineSymbol();
508 		if (!line_for_borders)
509 			line_for_borders = framing_line;
510 		fillCommonSymbolFields(framing_line, (OCADSymbol *)ocad_symbol);
511 
512 		// Basic line options
513 		framing_line->line_width = convertSize(ocad_symbol->fwidth);
514 		framing_line->color = convertColor(ocad_symbol->fcolor);
515 
516 		// Cap and join styles
517 		if (ocad_symbol->fstyle == 0)
518 		{
519 			framing_line->cap_style = LineSymbol::FlatCap;
520 			framing_line->join_style = LineSymbol::BevelJoin;
521 		}
522 		else if (ocad_symbol->fstyle == 1)
523 		{
524 			framing_line->cap_style = LineSymbol::RoundCap;
525 			framing_line->join_style = LineSymbol::RoundJoin;
526 		}
527 		else if (ocad_symbol->fstyle == 4)
528 		{
529 			framing_line->cap_style = LineSymbol::FlatCap;
530 			framing_line->join_style = LineSymbol::MiterJoin;
531 		}
532 	}
533 
534 	// Import a 'double' line?
535 	bool has_border_line = ocad_symbol->lwidth > 0 || ocad_symbol->rwidth > 0;
536 	LineSymbol *double_line = nullptr;
537 	if (ocad_symbol->dmode != 0 && (ocad_symbol->dflags & 1 || (has_border_line && !line_for_borders)))
538 	{
539 		double_line = new LineSymbol();
540 		line_for_borders = double_line;
541 		fillCommonSymbolFields(double_line, (OCADSymbol *)ocad_symbol);
542 
543 		double_line->line_width = convertSize(ocad_symbol->dwidth);
544 		if (ocad_symbol->dflags & 1)
545 			double_line->color = convertColor(ocad_symbol->dcolor);
546 		else
547 			double_line->color = nullptr;
548 
549 		double_line->cap_style = LineSymbol::FlatCap;
550 		double_line->join_style = LineSymbol::MiterJoin;
551 
552 		double_line->segment_length = convertSize(ocad_symbol->len);
553 		double_line->end_length = convertSize(ocad_symbol->elen);
554 	}
555 
556 	// Border lines
557 	if (has_border_line)
558 	{
559 		Q_ASSERT(line_for_borders);
560 		line_for_borders->have_border_lines = true;
561 		LineSymbolBorder& border = line_for_borders->getBorder();
562 		LineSymbolBorder& right_border = line_for_borders->getRightBorder();
563 
564 		// Border color and width
565 		border.color = convertColor(ocad_symbol->lcolor);
566 		border.width = convertSize(ocad_symbol->lwidth);
567 		border.shift = convertSize(ocad_symbol->lwidth) / 2 + (convertSize(ocad_symbol->dwidth) - line_for_borders->line_width) / 2;
568 
569 		right_border.color = convertColor(ocad_symbol->rcolor);
570 		right_border.width = convertSize(ocad_symbol->rwidth);
571 		right_border.shift = convertSize(ocad_symbol->rwidth) / 2 + (convertSize(ocad_symbol->dwidth) - line_for_borders->line_width) / 2;
572 
573 		// The borders may be dashed
574 		if (ocad_symbol->dgap > 0 && ocad_symbol->dmode > 1)
575 		{
576 			border.dashed = true;
577 			border.dash_length = convertSize(ocad_symbol->dlen);
578 			border.break_length = convertSize(ocad_symbol->dgap);
579 
580 			// If ocad_symbol->dmode == 2, only the left border should be dashed
581 			if (ocad_symbol->dmode > 2)
582 			{
583 				right_border.dashed = border.dashed;
584 				right_border.dash_length = border.dash_length;
585 				right_border.break_length = border.break_length;
586 			}
587 		}
588 	}
589 
590     // Create point symbols along line; middle ("normal") dash, corners, start, and end.
591     LineSymbol* symbol_line = main_line ? main_line : double_line;	// Find the line to attach the symbols to
592     if (!symbol_line)
593 	{
594 		main_line = new LineSymbol();
595 		symbol_line = main_line;
596 		fillCommonSymbolFields(main_line, (OCADSymbol *)ocad_symbol);
597 
598 		main_line->segment_length = convertSize(ocad_symbol->len);
599 		main_line->end_length = convertSize(ocad_symbol->elen);
600 	}
601     OCADPoint * symbolptr = (OCADPoint *)ocad_symbol->pts;
602 	symbol_line->mid_symbol = importPattern( ocad_symbol->smnpts, symbolptr);
603 	symbol_line->mid_symbols_per_spot = ocad_symbol->snum;
604 	symbol_line->mid_symbol_distance = convertSize(ocad_symbol->sdist);
605     symbolptr += ocad_symbol->smnpts;
606     if( ocad_symbol->ssnpts > 0 )
607     {
608 		//symbol_line->dash_symbol = importPattern( ocad_symbol->ssnpts, symbolptr);
609         symbolptr += ocad_symbol->ssnpts;
610     }
611     if( ocad_symbol->scnpts > 0 )
612     {
613 		symbol_line->dash_symbol = importPattern( ocad_symbol->scnpts, symbolptr);
614         symbol_line->dash_symbol->setName(QCoreApplication::translate("OpenOrienteering::LineSymbolSettings", "Dash symbol"));
615         symbolptr += ocad_symbol->scnpts;
616     }
617     if( ocad_symbol->sbnpts > 0 )
618     {
619 		symbol_line->start_symbol = importPattern( ocad_symbol->sbnpts, symbolptr);
620         symbol_line->start_symbol->setName(QCoreApplication::translate("OpenOrienteering::LineSymbolSettings", "Start symbol"));
621         symbolptr += ocad_symbol->sbnpts;
622     }
623     if( ocad_symbol->senpts > 0 )
624     {
625 		symbol_line->end_symbol = importPattern( ocad_symbol->senpts, symbolptr);
626     }
627     // FIXME: not really sure how this translates... need test cases
628     symbol_line->minimum_mid_symbol_count = 0; //1 + ocad_symbol->smin;
629 	symbol_line->minimum_mid_symbol_count_when_closed = 0; //1 + ocad_symbol->smin;
630 	symbol_line->show_at_least_one_symbol = false; // NOTE: this works in a different way than OCAD's 'at least X symbols' setting (per-segment instead of per-object)
631 
632 	// Suppress dash symbol at line ends if both start symbol and end symbol exist,
633 	// but don't create a warning unless a dash symbol is actually defined
634 	// and the line symbol is not Mapper's 799 Simple orienteering course.
635 	if (symbol_line->start_symbol && symbol_line->end_symbol)
636 	{
637 		symbol_line->setSuppressDashSymbolAtLineEnds(true);
638 		if (symbol_line->dash_symbol && symbol_line->getNumberComponent(0) != 799)
639 			addWarning(tr("Line symbol %1: suppressing dash symbol at line ends.").arg(QString::number(0.1 * ocad_symbol->number) + QLatin1Char(' ') + symbol_line->getName()));
640 	}
641 
642     // TODO: taper fields (tmode and tlast)
643 
644     if (!main_line && !framing_line)
645 		return double_line;
646 	else if (!double_line && !framing_line)
647 		return main_line;
648 	else if (!main_line && !double_line)
649 		return framing_line;
650 	else
651 	{
652 		CombinedSymbol* full_line = new CombinedSymbol();
653 		fillCommonSymbolFields(full_line, (OCADSymbol *)ocad_symbol);
654 		full_line->setNumParts(3);
655 		int part = 0;
656 		if (main_line)
657 		{
658 			full_line->setPart(part++, main_line, true);
659 			main_line->setHidden(false);
660 			main_line->setProtected(false);
661 		}
662 		if (double_line)
663 		{
664 			full_line->setPart(part++, double_line, true);
665 			double_line->setHidden(false);
666 			double_line->setProtected(false);
667 		}
668 		if (framing_line)
669 		{
670 			full_line->setPart(part++, framing_line, true);
671 			framing_line->setHidden(false);
672 			framing_line->setProtected(false);
673 		}
674 		full_line->setNumParts(part);
675 		return full_line;
676 	}
677 }
678 
importAreaSymbol(const OCADAreaSymbol * ocad_symbol)679 Symbol *OCAD8FileImport::importAreaSymbol(const OCADAreaSymbol *ocad_symbol)
680 {
681     AreaSymbol *symbol = new AreaSymbol();
682     fillCommonSymbolFields(symbol, (OCADSymbol *)ocad_symbol);
683 
684     // Basic area symbol fields: minimum_area, color
685     symbol->minimum_area = 0;
686     symbol->color = ocad_symbol->fill ? convertColor(ocad_symbol->color) : nullptr;
687     symbol->patterns.clear();
688     AreaSymbol::FillPattern *pat = nullptr;
689 
690     // Hatching
691     if (ocad_symbol->hmode > 0)
692     {
693         int n = symbol->patterns.size(); symbol->patterns.resize(n + 1); pat = &(symbol->patterns[n]);
694         pat->type = AreaSymbol::FillPattern::LinePattern;
695         pat->angle = convertRotation(ocad_symbol->hangle1);
696         pat->flags = AreaSymbol::FillPattern::Option::Rotatable;
697         pat->line_spacing = convertSize(ocad_symbol->hdist + ocad_symbol->hwidth);
698         pat->line_offset = 0;
699         pat->line_color = convertColor(ocad_symbol->hcolor);
700         pat->line_width = convertSize(ocad_symbol->hwidth);
701         if (ocad_symbol->hmode == 2)
702         {
703             // Second hatch, same as the first, just a different angle
704             symbol->patterns.push_back(*pat);
705             symbol->patterns.back().angle = convertRotation(ocad_symbol->hangle2);
706         }
707     }
708 
709     if (ocad_symbol->pmode > 0)
710     {
711         // OCAD 8 has a "staggered" pattern mode, where successive rows are shifted width/2 relative
712         // to each other. We need to simulate this in Mapper with two overlapping patterns, each with
713         // twice the height. The second is then offset by width/2, height/2.
714         auto spacing = convertSize(ocad_symbol->pheight);
715         if (ocad_symbol->pmode == 2) spacing *= 2;
716         int n = symbol->patterns.size(); symbol->patterns.resize(n + 1); pat = &(symbol->patterns[n]);
717         pat->type = AreaSymbol::FillPattern::PointPattern;
718         pat->angle = convertRotation(ocad_symbol->pangle);
719         pat->flags = AreaSymbol::FillPattern::Option::Rotatable;
720         pat->point_distance = convertSize(ocad_symbol->pwidth);
721         pat->line_spacing = spacing;
722         pat->line_offset = 0;
723         pat->offset_along_line = 0;
724         // FIXME: somebody needs to own this symbol and be responsible for deleting it
725         // Right now it looks like a potential memory leak
726         pat->point = importPattern(ocad_symbol->npts, (OCADPoint *)ocad_symbol->pts);
727         if (ocad_symbol->pmode == 2)
728         {
729             int n = symbol->patterns.size(); symbol->patterns.resize(n + 1); pat = &(symbol->patterns[n]);
730             pat->type = AreaSymbol::FillPattern::PointPattern;
731             pat->angle = convertRotation(ocad_symbol->pangle);
732             pat->flags = AreaSymbol::FillPattern::Option::Rotatable;
733             pat->point_distance = convertSize(ocad_symbol->pwidth);
734             pat->line_spacing = spacing;
735             pat->line_offset = pat->line_spacing / 2;
736             pat->offset_along_line = pat->point_distance / 2;
737             pat->point = importPattern(ocad_symbol->npts, (OCADPoint *)ocad_symbol->pts);
738         }
739     }
740 
741     return symbol;
742 }
743 
importTextSymbol(const OCADTextSymbol * ocad_symbol)744 Symbol *OCAD8FileImport::importTextSymbol(const OCADTextSymbol *ocad_symbol)
745 {
746     TextSymbol *symbol = new TextSymbol();
747     fillCommonSymbolFields(symbol, (OCADSymbol *)ocad_symbol);
748 
749     symbol->font_family = convertPascalString(ocad_symbol->font); // FIXME: font mapping?
750     symbol->color = convertColor(ocad_symbol->color);
751 	double d_font_size = (0.1 * ocad_symbol->dpts) / 72.0 * 25.4;
752 	symbol->font_size = qRound(1000 * d_font_size);
753     symbol->bold = (ocad_symbol->bold >= 550) ? true : false;
754     symbol->italic = (ocad_symbol->italic) ? true : false;
755     symbol->underline = false;
756 	symbol->paragraph_spacing = convertSize(ocad_symbol->pspace);
757 	symbol->character_spacing = ocad_symbol->cspace / 100.0;
758 	symbol->kerning = false;
759 	symbol->line_below = ocad_symbol->under;
760 	symbol->line_below_color = convertColor(ocad_symbol->ucolor);
761 	symbol->line_below_width = convertSize(ocad_symbol->uwidth);
762 	symbol->line_below_distance = convertSize(ocad_symbol->udist);
763 	symbol->custom_tabs.resize(ocad_symbol->ntabs);
764 	for (int i = 0; i < ocad_symbol->ntabs; ++i)
765 		symbol->custom_tabs[i] = convertSize(ocad_symbol->tab[i]);
766 
767 	int halign = (int)TextObject::AlignHCenter;
768 	if (ocad_symbol->halign == 0)
769 		halign = (int)TextObject::AlignLeft;
770 	else if (ocad_symbol->halign == 1)
771 		halign = (int)TextObject::AlignHCenter;
772 	else if (ocad_symbol->halign == 2)
773 		halign = (int)TextObject::AlignRight;
774 	else if (ocad_symbol->halign == 3)
775 	{
776 		// TODO: implement justified alignment
777 		addWarning(tr("During import of text symbol %1: ignoring justified alignment").arg(0.1 * ocad_symbol->number));
778 	}
779 	text_halign_map[symbol] = halign;
780 
781     if (ocad_symbol->bold != 400 && ocad_symbol->bold != 700)
782     {
783         addWarning(tr("During import of text symbol %1: ignoring custom weight (%2)")
784                        .arg(0.1 * ocad_symbol->number).arg(ocad_symbol->bold));
785     }
786     if (ocad_symbol->cspace != 0)
787 	{
788 		addWarning(tr("During import of text symbol %1: custom character spacing is set, its implementation does not match OCAD's behavior yet")
789 		.arg(0.1 * ocad_symbol->number));
790 	}
791     if (ocad_symbol->wspace != 100)
792     {
793         addWarning(tr("During import of text symbol %1: ignoring custom word spacing (%2%)")
794                        .arg(0.1 * ocad_symbol->number).arg(ocad_symbol->wspace));
795     }
796     if (ocad_symbol->indent1 != 0 || ocad_symbol->indent2 != 0)
797     {
798         addWarning(tr("During import of text symbol %1: ignoring custom indents (%2/%3)")
799                        .arg(0.1 * ocad_symbol->number).arg(ocad_symbol->indent1).arg(ocad_symbol->indent2));
800     }
801 
802 	if (ocad_symbol->fmode > 0)
803 	{
804 		symbol->framing = true;
805 		symbol->framing_color = convertColor(ocad_symbol->fcolor);
806 		if (ocad_symbol->fmode == 1)
807 		{
808 			symbol->framing_mode = TextSymbol::ShadowFraming;
809 			symbol->framing_shadow_x_offset = convertSize(ocad_symbol->fdx);
810 			symbol->framing_shadow_y_offset = -1 * convertSize(ocad_symbol->fdy);
811 		}
812 		else if (ocad_symbol->fmode == 2)
813 		{
814 			symbol->framing_mode = TextSymbol::LineFraming;
815 			symbol->framing_line_half_width = convertSize(ocad_symbol->fdpts);
816 		}
817 		else
818 		{
819 			addWarning(tr("During import of text symbol %1: ignoring text framing (mode %2)")
820 			.arg(0.1 * ocad_symbol->number).arg(ocad_symbol->fmode));
821 		}
822 	}
823 
824     symbol->updateQFont();
825 
826 	// Convert line spacing
827 	double absolute_line_spacing = d_font_size * 0.01 * ocad_symbol->lspace;
828 	symbol->line_spacing = absolute_line_spacing / (symbol->getFontMetrics().lineSpacing() / symbol->calculateInternalScaling());
829 
830     return symbol;
831 }
importRectSymbol(const OCADRectSymbol * ocad_symbol)832 OCAD8FileImport::RectangleInfo* OCAD8FileImport::importRectSymbol(const OCADRectSymbol* ocad_symbol)
833 {
834 	RectangleInfo rect;
835 	rect.border_line = new LineSymbol();
836 	fillCommonSymbolFields(rect.border_line, (OCADSymbol *)ocad_symbol);
837 	rect.border_line->line_width = convertSize(ocad_symbol->width);
838 	rect.border_line->color = convertColor(ocad_symbol->color);
839 	rect.border_line->cap_style = LineSymbol::FlatCap;
840 	rect.border_line->join_style = LineSymbol::RoundJoin;
841 	rect.corner_radius = 0.001 * convertSize(ocad_symbol->corner);
842 	rect.has_grid = ocad_symbol->flags & 1;
843 
844 	if (rect.has_grid)
845 	{
846 		rect.inner_line = new LineSymbol();
847 		fillCommonSymbolFields(rect.inner_line, (OCADSymbol *)ocad_symbol);
848 		rect.inner_line->setNumberComponent(2, 1);
849 		rect.inner_line->line_width = qRound(1000 * 0.15);
850 		rect.inner_line->color = rect.border_line->color;
851 
852 		rect.text = new TextSymbol();
853 		fillCommonSymbolFields(rect.text, (OCADSymbol *)ocad_symbol);
854 		rect.text->setNumberComponent(2, 2);
855 		rect.text->font_family = QString::fromLatin1("Arial");
856 		rect.text->font_size = qRound(1000 * (15 / 72.0 * 25.4));
857 		rect.text->color = rect.border_line->color;
858 		rect.text->bold = true;
859 		rect.text->updateQFont();
860 
861 		rect.number_from_bottom = ocad_symbol->flags & 2;
862 		rect.cell_width = 0.001 * convertSize(ocad_symbol->cwidth);
863 		rect.cell_height = 0.001 * convertSize(ocad_symbol->cheight);
864 		rect.unnumbered_cells = ocad_symbol->gcells;
865 		rect.unnumbered_text = convertPascalString(ocad_symbol->gtext);
866 	}
867 
868 	return &rectangle_info.insert(ocad_symbol->number, rect).value();
869 }
870 
importPattern(s16 npts,OCADPoint * pts)871 PointSymbol *OCAD8FileImport::importPattern(s16 npts, OCADPoint *pts)
872 {
873     PointSymbol *symbol = new PointSymbol();
874     symbol->setRotatable(true);
875     OCADPoint *p = pts, *end = pts + npts;
876     while (p < end) {
877         OCADSymbolElement *elt = (OCADSymbolElement *)p;
878         int element_index = symbol->getNumElements();
879 		bool multiple_elements = p + (2 + elt->npts) < end || p > pts;
880         if (elt->type == OCAD_DOT_ELEMENT)
881         {
882 			int inner_radius = (int)convertSize(elt->diameter) / 2;
883 			if (inner_radius > 0)
884 			{
885 				PointSymbol* element_symbol = multiple_elements ? (new PointSymbol()) : symbol;
886 				element_symbol->inner_color = convertColor(elt->color);
887 				element_symbol->inner_radius = inner_radius;
888 				element_symbol->outer_color = nullptr;
889 				element_symbol->outer_width = 0;
890 				if (multiple_elements)
891 				{
892 					element_symbol->setRotatable(false);
893 					PointObject* element_object = new PointObject(element_symbol);
894 					element_object->coords.resize(1);
895 					symbol->addElement(element_index, element_object, element_symbol);
896 				}
897 			}
898         }
899         else if (elt->type == OCAD_CIRCLE_ELEMENT)
900         {
901 			int inner_radius = (int)convertSize(elt->diameter) / 2 - (int)convertSize(elt->width);
902 			int outer_width = (int)convertSize(elt->width);
903 			if (outer_width > 0 && inner_radius > 0)
904 			{
905 				PointSymbol* element_symbol = (multiple_elements) ? (new PointSymbol()) : symbol;
906 				element_symbol->inner_color = nullptr;
907 				element_symbol->inner_radius = inner_radius;
908 				element_symbol->outer_color = convertColor(elt->color);
909 				element_symbol->outer_width = outer_width;
910 				if (multiple_elements)
911 				{
912 					element_symbol->setRotatable(false);
913 					PointObject* element_object = new PointObject(element_symbol);
914 					element_object->coords.resize(1);
915 					symbol->addElement(element_index, element_object, element_symbol);
916 				}
917 			}
918         }
919         else if (elt->type == OCAD_LINE_ELEMENT)
920         {
921             LineSymbol* element_symbol = new LineSymbol();
922             element_symbol->line_width = convertSize(elt->width);
923             element_symbol->color = convertColor(elt->color);
924             PathObject* element_object = new PathObject(element_symbol);
925             fillPathCoords(element_object, false, elt->npts, elt->pts);
926 			element_object->recalculateParts();
927             symbol->addElement(element_index, element_object, element_symbol);
928         }
929         else if (elt->type == OCAD_AREA_ELEMENT)
930         {
931             AreaSymbol* element_symbol = new AreaSymbol();
932             element_symbol->color = convertColor(elt->color);
933             PathObject* element_object = new PathObject(element_symbol);
934             fillPathCoords(element_object, true, elt->npts, elt->pts);
935 			element_object->recalculateParts();
936             symbol->addElement(element_index, element_object, element_symbol);
937         }
938         p += (2 + elt->npts);
939     }
940     return symbol;
941 }
942 
943 
fillCommonSymbolFields(Symbol * symbol,const OCADSymbol * ocad_symbol)944 void OCAD8FileImport::fillCommonSymbolFields(Symbol *symbol, const OCADSymbol *ocad_symbol)
945 {
946     // common fields are name, number, description, helper_symbol, hidden/protected status
947     symbol->setName(convertPascalString(ocad_symbol->name));
948     symbol->setNumberComponent(0, ocad_symbol->number / 10);
949     symbol->setNumberComponent(1, ocad_symbol->number % 10);
950     symbol->setNumberComponent(2, -1);
951     symbol->setIsHelperSymbol(false); // no such thing in OCAD
952     if (ocad_symbol->status & 1)
953 		symbol->setProtected(true);
954 	if (ocad_symbol->status & 2)
955 		symbol->setHidden(true);
956 }
957 
importObject(const OCADObject * ocad_object,MapPart * part)958 Object *OCAD8FileImport::importObject(const OCADObject* ocad_object, MapPart* part)
959 {
960 	Symbol* symbol;
961     if (!symbol_index.contains(ocad_object->symbol))
962     {
963 		if (!rectangle_info.contains(ocad_object->symbol))
964 		{
965 			if (ocad_object->type == 1)
966 				symbol = map->getUndefinedPoint();
967 			else if (ocad_object->type == 2 || ocad_object->type == 3)
968 				symbol = map->getUndefinedLine();
969 			else if (ocad_object->type == 4 || ocad_object->type == 5)
970 				symbol = map->getUndefinedText();
971 			else
972 			{
973 				addWarning(tr("Unable to load object"));
974 				return nullptr;
975 			}
976 		}
977 		else
978 		{
979 			if (!importRectangleObject(ocad_object, part, rectangle_info[ocad_object->symbol]))
980 				addWarning(tr("Unable to import rectangle object"));
981 			return nullptr;
982 		}
983     }
984     else
985 		symbol = symbol_index[ocad_object->symbol];
986 
987     if (symbol->getType() == Symbol::Point)
988     {
989         PointObject *p = new PointObject();
990         p->symbol = symbol;
991 
992         // extra properties: rotation
993 		PointSymbol* point_symbol = reinterpret_cast<PointSymbol*>(symbol);
994 		if (point_symbol->isRotatable())
995 			p->setRotation(convertRotation(ocad_object->angle));
996 		else if (ocad_object->angle != 0)
997 		{
998 			if (!point_symbol->isSymmetrical())
999 			{
1000 				point_symbol->setRotatable(true);
1001 				p->setRotation(convertRotation(ocad_object->angle));
1002 			}
1003 		}
1004 
1005         // only 1 coordinate is allowed, enforce it even if the OCAD object claims more.
1006 		fillPathCoords(p, false, 1, ocad_object->pts);
1007         p->setMap(map);
1008         return p;
1009     }
1010     else if (symbol->getType() == Symbol::Text)
1011     {
1012 		TextObject *t = new TextObject(symbol);
1013 
1014         // extra properties: rotation, horizontalAlignment, verticalAlignment, text
1015         t->setRotation(convertRotation(ocad_object->angle));
1016 		t->setHorizontalAlignment((TextObject::HorizontalAlignment)text_halign_map.value(symbol));
1017 		t->setVerticalAlignment(TextObject::AlignBaseline);
1018 
1019         const char *text_ptr = (const char *)(ocad_object->pts + ocad_object->npts);
1020         std::size_t text_len = sizeof(OCADPoint) * ocad_object->ntext;
1021         if (ocad_object->unicode) t->setText(convertWideCString(text_ptr, text_len, true));
1022         else t->setText(convertCString(text_ptr, text_len, true));
1023 
1024         // Text objects need special path translation
1025         if (!fillTextPathCoords(t, reinterpret_cast<TextSymbol*>(symbol), ocad_object->npts, (OCADPoint *)ocad_object->pts))
1026         {
1027             addWarning(tr("Not importing text symbol, couldn't figure out path' (npts=%1): %2")
1028                            .arg(ocad_object->npts).arg(t->getText()));
1029             delete t;
1030             return nullptr;
1031         }
1032         t->setMap(map);
1033         return t;
1034     }
1035     else if (symbol->getType() == Symbol::Line || symbol->getType() == Symbol::Area || symbol->getType() == Symbol::Combined) {
1036 		PathObject *p = new PathObject(symbol);
1037 
1038 		p->setPatternRotation(convertRotation(ocad_object->angle));
1039 
1040 		// Normal path
1041 		fillPathCoords(p, symbol->getType() == Symbol::Area, ocad_object->npts, ocad_object->pts);
1042 		p->recalculateParts();
1043 		p->setMap(map);
1044 		return p;
1045     }
1046 
1047     return nullptr;
1048 }
1049 
importRectangleObject(const OCADObject * ocad_object,MapPart * part,const OCAD8FileImport::RectangleInfo & rect)1050 bool OCAD8FileImport::importRectangleObject(const OCADObject* ocad_object, MapPart* part, const OCAD8FileImport::RectangleInfo& rect)
1051 {
1052 	if (ocad_object->npts != 4)
1053 		return false;
1054 
1055 	// Convert corner points
1056 #ifdef Q_CC_CLANG
1057 #pragma clang diagnostic push
1058 #pragma clang diagnostic ignored "-Warray-bounds"
1059 #endif
1060 	s32 buf[3];
1061 	ocad_point(buf, &(ocad_object->pts[3]));
1062 	MapCoord top_left;
1063 	convertPoint(top_left, buf[0], buf[1]);
1064 	ocad_point(buf, &(ocad_object->pts[0]));
1065 	MapCoord bottom_left;
1066 	convertPoint(bottom_left, buf[0], buf[1]);
1067 	ocad_point(buf, &(ocad_object->pts[2]));
1068 	MapCoord top_right;
1069 	convertPoint(top_right, buf[0], buf[1]);
1070 	ocad_point(buf, &(ocad_object->pts[1]));
1071 	MapCoord bottom_right;
1072 	convertPoint(bottom_right, buf[0], buf[1]);
1073 #ifdef Q_CC_CLANG
1074 #pragma clang diagnostic pop
1075 #endif
1076 
1077 	MapCoordF top_left_f = MapCoordF(top_left);
1078 	MapCoordF top_right_f = MapCoordF(top_right);
1079 	MapCoordF bottom_left_f = MapCoordF(bottom_left);
1080 	MapCoordF bottom_right_f = MapCoordF(bottom_right);
1081 	MapCoordF right = MapCoordF(top_right.x() - top_left.x(), top_right.y() - top_left.y());
1082 	double angle = right.angle();
1083 	MapCoordF down = MapCoordF(bottom_left.x() - top_left.x(), bottom_left.y() - top_left.y());
1084 	right.normalize();
1085 	down.normalize();
1086 
1087 	// Create border line
1088 	MapCoordVector coords;
1089 	if (rect.corner_radius == 0)
1090 	{
1091 		coords.emplace_back(top_left);
1092 		coords.emplace_back(top_right);
1093 		coords.emplace_back(bottom_right);
1094 		coords.emplace_back(bottom_left);
1095 	}
1096 	else
1097 	{
1098 		double handle_radius = (1 - BEZIER_KAPPA) * rect.corner_radius;
1099 		coords.emplace_back(top_right_f - right * rect.corner_radius, MapCoord::CurveStart);
1100 		coords.emplace_back(top_right_f - right * handle_radius);
1101 		coords.emplace_back(top_right_f + down * handle_radius);
1102 		coords.emplace_back(top_right_f + down * rect.corner_radius);
1103 		coords.emplace_back(bottom_right_f - down * rect.corner_radius, MapCoord::CurveStart);
1104 		coords.emplace_back(bottom_right_f - down * handle_radius);
1105 		coords.emplace_back(bottom_right_f - right * handle_radius);
1106 		coords.emplace_back(bottom_right_f - right * rect.corner_radius);
1107 		coords.emplace_back(bottom_left_f + right * rect.corner_radius, MapCoord::CurveStart);
1108 		coords.emplace_back(bottom_left_f + right * handle_radius);
1109 		coords.emplace_back(bottom_left_f - down * handle_radius);
1110 		coords.emplace_back(bottom_left_f - down * rect.corner_radius);
1111 		coords.emplace_back(top_left_f + down * rect.corner_radius, MapCoord::CurveStart);
1112 		coords.emplace_back(top_left_f + down * handle_radius);
1113 		coords.emplace_back(top_left_f + right * handle_radius);
1114 		coords.emplace_back(top_left_f + right * rect.corner_radius);
1115 	}
1116 	PathObject *border_path = new PathObject(rect.border_line, coords, map);
1117 	border_path->parts().front().setClosed(true, false);
1118 	part->objects.push_back(border_path);
1119 
1120 	if (rect.has_grid && rect.cell_width > 0 && rect.cell_height > 0)
1121 	{
1122 		// Calculate grid sizes
1123 		double width = top_left.distanceTo(top_right);
1124 		double height = top_left.distanceTo(bottom_left);
1125 		int num_cells_x = qMax(1, qRound(width / rect.cell_width));
1126 		int num_cells_y = qMax(1, qRound(height / rect.cell_height));
1127 
1128 		float cell_width = width / num_cells_x;
1129 		float cell_height = height / num_cells_y;
1130 
1131 		// Create grid lines
1132 		coords.resize(2);
1133 		for (int x = 1; x < num_cells_x; ++x)
1134 		{
1135 			coords[0] = MapCoord(top_left_f + x * cell_width * right);
1136 			coords[1] = MapCoord(bottom_left_f + x * cell_width * right);
1137 
1138 			PathObject *path = new PathObject(rect.inner_line, coords, map);
1139 			part->objects.push_back(path);
1140 		}
1141 		for (int y = 1; y < num_cells_y; ++y)
1142 		{
1143 			coords[0] = MapCoord(top_left_f + y * cell_height * down);
1144 			coords[1] = MapCoord(top_right_f + y * cell_height * down);
1145 
1146 			PathObject *path = new PathObject(rect.inner_line, coords, map);
1147 			part->objects.push_back(path);
1148 		}
1149 
1150 		// Create grid text
1151 		if (height >= rect.cell_height / 2)
1152 		{
1153 			for (int y = 0; y < num_cells_y; ++y)
1154 			{
1155 				for (int x = 0; x < num_cells_x; ++x)
1156 				{
1157 					int cell_num;
1158 					QString cell_text;
1159 
1160 					if (rect.number_from_bottom)
1161 						cell_num = y * num_cells_x + x + 1;
1162 					else
1163 						cell_num = (num_cells_y - 1 - y) * num_cells_x + x + 1;
1164 
1165 					if (cell_num > num_cells_x * num_cells_y - rect.unnumbered_cells)
1166 						cell_text = rect.unnumbered_text;
1167 					else
1168 						cell_text = QString::number(cell_num);
1169 
1170 					TextObject* object = new TextObject(rect.text);
1171 					object->setMap(map);
1172 					object->setText(cell_text);
1173 					object->setRotation(-angle);
1174 					object->setHorizontalAlignment(TextObject::AlignLeft);
1175 					object->setVerticalAlignment(TextObject::AlignTop);
1176 					double position_x = (x + 0.07f) * cell_width;
1177 					double position_y = (y + 0.04f) * cell_height + rect.text->getFontMetrics().ascent() / rect.text->calculateInternalScaling() - rect.text->getFontSize();
1178 					object->setAnchorPosition(top_left_f + position_x * right + position_y * down);
1179 					part->objects.push_back(object);
1180 
1181 					//pts[0].Y -= rectinfo.gridText.FontAscent - rectinfo.gridText.FontEmHeight;
1182 				}
1183 			}
1184 		}
1185 	}
1186 
1187 	return true;
1188 }
1189 
importString(OCADStringEntry * entry)1190 void OCAD8FileImport::importString(OCADStringEntry *entry)
1191 {
1192 	OCADCString *ocad_str = ocad_string(file, entry);
1193 	if (entry->type == 8)
1194 	{
1195 		// Template
1196 		importTemplate(ocad_str);
1197 	}
1198 
1199 	// TODO: parse more types of strings, maybe the print parameters?
1200 }
1201 
importTemplate(OCADCString * ocad_str)1202 Template *OCAD8FileImport::importTemplate(OCADCString* ocad_str)
1203 {
1204 	Template* templ = nullptr;
1205 	QByteArray data(ocad_str->str); // copies the data.
1206 	QString filename = encoding_1byte->toUnicode(data.left(data.indexOf('\t', 0)));
1207 	QString clean_path = QDir::cleanPath(QString(filename).replace(QLatin1Char('\\'), QLatin1Char('/')));
1208 	QString extension = QFileInfo(clean_path).suffix();
1209 	if (extension.compare(QLatin1String("ocd"), Qt::CaseInsensitive) == 0)
1210 	{
1211 		templ = new TemplateMap(clean_path, map);
1212 	}
1213 	else if (QImageReader::supportedImageFormats().contains(extension.toLatin1()))
1214 	{
1215 		templ = new TemplateImage(clean_path, map);
1216 	}
1217 	else
1218 	{
1219 		addWarning(tr("Unable to import template: background \"%1\" doesn't seem to be a raster image").arg(filename));
1220 		return nullptr;
1221 	}
1222 
1223 	OCADBackground background = importBackground(data);
1224 	MapCoord c;
1225 	convertPoint(c, background.trnx, background.trny);
1226 	templ->setTemplatePosition(c);
1227 	templ->setTemplateRotation(M_PI / 180 * background.angle);
1228 	templ->setTemplateScaleX(convertTemplateScale(background.sclx));
1229 	templ->setTemplateScaleY(convertTemplateScale(background.scly));
1230 	templ->setTemplateShear(0.0);
1231 
1232 	map->templates.insert(map->templates.begin(), templ);
1233 
1234 	if (view)
1235 	{
1236 		auto opacity = qMax(0.0, qMin(1.0, 0.01 * (100 - background.dimming)));
1237 		view->setTemplateVisibility(templ, { float(opacity), bool(background.s) });
1238 	}
1239 
1240 	return templ;
1241 }
1242 
1243 // A more flexible reimplementation of libocad's ocad_to_background().
importBackground(const QByteArray & data)1244 OCADBackground OCAD8FileImport::importBackground(const QByteArray& data)
1245 {
1246 	unsigned int num_angles = 0;
1247 	OCADBackground background;
1248 	background.filename = data.data(); // tab-terminated, not 0-terminated!
1249 	background.trnx = 0;
1250 	background.trny = 0;
1251 	background.angle = 0.0;
1252 	background.sclx = 100.0;
1253 	background.scly = 100.0;
1254 	background.dimming = 0;
1255 	background.s = 1;
1256 
1257 	int i = data.indexOf('\t', 0);
1258 	while (i >= 0)
1259 	{
1260 		double value;
1261 		bool ok;
1262 		int next_i = data.indexOf('\t', i+1);
1263 		int len = (next_i > 0 ? next_i : data.length()) - i - 2;
1264 		switch (data[i+1])
1265 		{
1266 			case 'x':
1267 				background.trnx = qRound(data.mid(i + 2, len).toDouble());
1268 				break;
1269 			case 'y':
1270 				background.trny = qRound(data.mid(i + 2, len).toDouble());
1271 				break;
1272 			case 'a':
1273 			case 'b':
1274 				// TODO: use the distinct angles correctly, not just the average
1275 				background.angle += data.mid(i + 2, len).toDouble(&ok);
1276 				if (ok)
1277 					++num_angles;
1278 				break;
1279 			case 'u':
1280 				value = data.mid(i + 2, len).toDouble(&ok);
1281 				if (ok && qAbs(value) >= 0.0001)
1282 					background.sclx = value;
1283 				break;
1284 			case 'v':
1285 				value = data.mid(i + 2, len).toDouble(&ok);
1286 				if (ok && qAbs(value) >= 0.0001)
1287 					background.scly = value;
1288 				break;
1289 			case 'd':
1290 				background.dimming = data.mid(i + 2, len).toInt();
1291 				break;
1292 			case 's':
1293 				background.s = data.mid(i + 2, len).toInt();
1294 				break;
1295 			default:
1296 				; // nothing
1297 		}
1298 		i = next_i;
1299 	}
1300 
1301 	if (num_angles)
1302 		background.angle = background.angle / num_angles;
1303 
1304 	return background;
1305 }
1306 
importRasterTemplate(const OCADBackground & background)1307 Template *OCAD8FileImport::importRasterTemplate(const OCADBackground &background)
1308 {
1309 	QString filename(encoding_1byte->toUnicode(background.filename));
1310 	filename = QDir::cleanPath(filename.replace(QLatin1Char('\\'), QLatin1Char('/')));
1311 	if (isRasterImageFile(filename))
1312     {
1313         TemplateImage* templ = new TemplateImage(filename, map);
1314         MapCoord c;
1315         convertPoint(c, background.trnx, background.trny);
1316         templ->setTemplateX(c.nativeX());
1317         templ->setTemplateY(c.nativeY());
1318         templ->setTemplateRotation(M_PI / 180 * background.angle);
1319         templ->setTemplateScaleX(convertTemplateScale(background.sclx));
1320         templ->setTemplateScaleY(convertTemplateScale(background.scly));
1321         templ->setTemplateShear(0.0);
1322         // FIXME: import template view parameters: background.dimming and background.transparent
1323 		// TODO: import template visibility
1324         return templ;
1325     }
1326     else
1327     {
1328         addWarning(tr("Unable to import template: background \"%1\" doesn't seem to be a raster image").arg(filename));
1329     }
1330     return nullptr;
1331 }
1332 
setPathHolePoint(Object * object,int i)1333 void OCAD8FileImport::setPathHolePoint(Object *object, int i)
1334 {
1335 	// Look for curve start points before the current point and apply hole point only if no such point is there.
1336 	// This prevents hole points in the middle of a curve caused by incorrect map objects.
1337 	if (i >= 1 && object->coords[i].isCurveStart())
1338 		; //object->coords[i-1].setHolePoint(true);
1339 	else if (i >= 2 && object->coords[i-1].isCurveStart())
1340 		; //object->coords[i-2].setHolePoint(true);
1341 	else if (i >= 3 && object->coords[i-2].isCurveStart())
1342 		; //object->coords[i-3].setHolePoint(true);
1343 	else
1344 		object->coords[i].setHolePoint(true);
1345 }
1346 
1347 /** Translates the OCAD path given in the last two arguments into an Object.
1348  */
fillPathCoords(Object * object,bool is_area,u16 npts,const OCADPoint * pts)1349 void OCAD8FileImport::fillPathCoords(Object *object, bool is_area, u16 npts, const OCADPoint *pts)
1350 {
1351     object->coords.resize(npts);
1352     s32 buf[3];
1353     for (int i = 0; i < npts; i++)
1354     {
1355         ocad_point(buf, &(pts[i]));
1356         MapCoord &coord = object->coords[i];
1357         convertPoint(coord, buf[0], buf[1]);
1358         // We can support CurveStart, HolePoint, DashPoint.
1359         // CurveStart needs to be applied to the main point though, not the control point, and
1360 		// hole points need to bet set as the last point of a part of an area object instead of the first point of the next part
1361 		if (buf[2] & PX_CTL1 && i > 0)
1362 			object->coords[i-1].setCurveStart(true);
1363 		if ((buf[2] & (PY_DASH << 8)) || (buf[2] & (PY_CORNER << 8)))
1364 			coord.setDashPoint(true);
1365 		if (buf[2] & (PY_HOLE << 8))
1366 			setPathHolePoint(object, is_area ? (i - 1) : i);
1367     }
1368 
1369     // For path objects, create closed parts where the position of the last point is equal to that of the first point
1370     if (object->getType() == Object::Path)
1371 	{
1372 		int start = 0;
1373 		for (int i = 0; i < (int)object->coords.size(); ++i)
1374 		{
1375 			if (!object->coords[i].isHolePoint() && i < (int)object->coords.size() - 1)
1376 				continue;
1377 
1378 			if (object->coords[i].isPositionEqualTo(object->coords[start]))
1379 			{
1380 				MapCoord coord = object->coords[start];
1381 				coord.setCurveStart(false);
1382 				coord.setHolePoint(true);
1383 				coord.setClosePoint(true);
1384 				object->coords[i] = coord;
1385 			}
1386 
1387 			start = i + 1;
1388 		}
1389 	}
1390 }
1391 
1392 /** Translates an OCAD text object path into a Mapper text object specifier, if possible.
1393  *  If successful, sets either 1 or 2 coordinates in the text object and returns true.
1394  *  If the OCAD path was not importable, leaves the TextObject alone and returns false.
1395  */
fillTextPathCoords(TextObject * object,TextSymbol * symbol,u16 npts,OCADPoint * pts)1396 bool OCAD8FileImport::fillTextPathCoords(TextObject *object, TextSymbol *symbol, u16 npts, OCADPoint *pts)
1397 {
1398     // text objects either have 1 point (free anchor) or 2 (midpoint/size)
1399     // OCAD appears to always have 5 or 4 points (possible single anchor, then 4 corner coordinates going clockwise from anchor).
1400     if (npts == 0) return false;
1401 
1402 	if (npts == 4)
1403 	{
1404 		// Box text
1405 		s32 buf[3];
1406 		ocad_point(buf, &(pts[3]));
1407 		MapCoord top_left;
1408 		convertPoint(top_left, buf[0], buf[1]);
1409 		ocad_point(buf, &(pts[0]));
1410 		MapCoord bottom_left;
1411 		convertPoint(bottom_left, buf[0], buf[1]);
1412 		ocad_point(buf, &(pts[2]));
1413 		MapCoord top_right;
1414 		convertPoint(top_right, buf[0], buf[1]);
1415 
1416 		// According to Purple Pen source code: OCAD adds an extra internal leading (incorrectly).
1417 		QFontMetricsF metrics = symbol->getFontMetrics();
1418 		double top_adjust = -symbol->getFontSize() + (metrics.ascent() + metrics.descent() + 0.5) / symbol->calculateInternalScaling();
1419 
1420 		MapCoordF adjust_vector = MapCoordF(top_adjust * sin(object->getRotation()), top_adjust * cos(object->getRotation()));
1421 		top_left = MapCoord(top_left.x() + adjust_vector.x(), top_left.y() + adjust_vector.y());
1422 		top_right = MapCoord(top_right.x() + adjust_vector.x(), top_right.y() + adjust_vector.y());
1423 
1424 		object->setBox((bottom_left.nativeX() + top_right.nativeX()) / 2, (bottom_left.nativeY() + top_right.nativeY()) / 2,
1425 					   top_left.distanceTo(top_right), top_left.distanceTo(bottom_left));
1426 
1427 		object->setVerticalAlignment(TextObject::AlignTop);
1428 	}
1429 	else
1430 	{
1431 		// Single anchor text
1432 		if (npts != 5)
1433 			addWarning(tr("Trying to import a text object with unknown coordinate format"));
1434 
1435 		s32 buf[3];
1436 		ocad_point(buf, &(pts[0])); // anchor point
1437 
1438 		MapCoord coord;
1439 		convertPoint(coord, buf[0], buf[1]);
1440 		object->setAnchorPosition(coord.nativeX(), coord.nativeY());
1441 
1442 		object->setVerticalAlignment(TextObject::AlignBaseline);
1443 	}
1444 
1445     return true;
1446 }
1447 
1448 /** Converts a single-byte-per-character, length-payload string to a QString.
1449  *
1450  *  The byte sequence will be: LEN C0 C1 C2 C3...
1451  *
1452  *  Obviously this will only hold up to 255 characters. By default, we interpret the
1453  *  bytes using Windows-1252, as that's the most likely candidate for OCAD files in
1454  *  the wild.
1455  */
convertPascalString(const char * p)1456 QString OCAD8FileImport::convertPascalString(const char *p) {
1457     int len = *((unsigned char *)p);
1458     return encoding_1byte->toUnicode((p + 1), len);
1459 }
1460 
1461 /** Converts a single-byte-per-character, zero-terminated string to a QString.
1462  *
1463  *  The byte sequence will be: C0 C1 C2 C3 ... 00. n describes the maximum
1464  *  length (in bytes) that will be scanned for a zero terminator; if none is found,
1465  *  the string will be truncated at that location.
1466  */
convertCString(const char * p,std::size_t n,bool ignore_first_newline)1467 QString OCAD8FileImport::convertCString(const char *p, std::size_t n, bool ignore_first_newline) {
1468     size_t i = 0;
1469     for (; i < n; i++) {
1470         if (p[i] == 0) break;
1471     }
1472     if (ignore_first_newline && n >= 2 && p[0] == '\r' && p[1] == '\n')
1473 	{
1474 		// Remove "\r\n" at the beginning of texts, somehow OCAD seems to add this sometimes but ignores it
1475 		p += 2;
1476 		i -= 2;
1477 	}
1478     return encoding_1byte->toUnicode(p, i);
1479 }
1480 
1481 /** Converts a two-byte-per-character, zero-terminated string to a QString. By default,
1482  *  we interpret the bytes using UTF-16LE, as that's the most likely candidate for
1483  *  OCAD files in the wild.
1484  *
1485  *  The byte sequence will be: L0 H0 L1 H1 L2 H2... 00 00. n describes the maximum
1486  *  length (in bytes) that will be scanned for a zero terminator; if none is found,
1487  *  the string will be truncated at that location.
1488  */
convertWideCString(const char * p,std::size_t n,bool ignore_first_newline)1489 QString OCAD8FileImport::convertWideCString(const char *p, std::size_t n, bool ignore_first_newline) {
1490     const u16 *q = (const u16 *)p;
1491     size_t i = 0;
1492     for (; i < n; i++) {
1493         if (q[i] == 0) break;
1494     }
1495     if (ignore_first_newline && n >= 4 && p[0] == '\r' && p[2] == '\n')
1496 	{
1497 		// Remove "\r\n" at the beginning of texts, somehow OCAD seems to add this sometimes but ignores it
1498 		p += 4;
1499 		i -= 2;
1500 	}
1501     return encoding_2byte->toUnicode(p, i * 2);
1502 }
1503 
convertRotation(int angle)1504 float OCAD8FileImport::convertRotation(int angle) {
1505     // OCAD uses tenths of a degree, counterclockwise
1506     // BUG: if sin(rotation) is < 0 for a hatched area pattern, the pattern's createRenderables() will go into an infinite loop.
1507     // So until that's fixed, we keep a between 0 and PI
1508     double a = (M_PI / 180) *  (0.1f * angle);
1509     while (a < 0) a += 2 * M_PI;
1510     //if (a < 0 || a > M_PI) qDebug() << "Found angle" << a;
1511     return (float)a;
1512 }
1513 
convertPoint(MapCoord & coord,s32 ocad_x,s32 ocad_y)1514 void OCAD8FileImport::convertPoint(MapCoord &coord, s32 ocad_x, s32 ocad_y)
1515 {
1516     // Recover from broken coordinate export from Mapper 0.6.2 ... 0.6.4 (#749)
1517     // Cf. broken::convertPointMember below:
1518     // The values -4 ... -1 (-0.004 mm ... -0.001 mm) were converted to 0x80000000u instead of 0.
1519     // This is the maximum value. Thus it is okay to assume it won't occur in regular data,
1520     // and we can safely replace it with 0 here.
1521     // But the input parameter were already subject to right shift in ocad_point ...
1522     constexpr auto invalid_value = s32(0x80000000u) >> 8; // ... so we use this value here.
1523     if (ocad_x == invalid_value)
1524         ocad_x = 0;
1525     if (ocad_y == invalid_value)
1526         ocad_y = 0;
1527 
1528     // OCAD uses hundredths of a millimeter.
1529     // oo-mapper uses 1/1000 mm
1530     coord.setNativeX(offset_x + (qint32)ocad_x * 10);
1531     // Y-axis is flipped.
1532     coord.setNativeY(offset_y + (qint32)ocad_y * (-10));
1533 }
1534 
convertSize(int ocad_size)1535 qint32 OCAD8FileImport::convertSize(int ocad_size) {
1536     // OCAD uses hundredths of a millimeter.
1537     // oo-mapper uses 1/1000 mm
1538     return ((qint32)ocad_size) * 10;
1539 }
1540 
convertColor(int color)1541 const MapColor *OCAD8FileImport::convertColor(int color) {
1542 	if (!color_index.contains(color))
1543 	{
1544 		addWarning(tr("Color id not found: %1, ignoring this color").arg(color));
1545 		return nullptr;
1546 	}
1547 	else
1548 		return color_index[color];
1549 }
1550 
convertTemplateScale(double ocad_scale)1551 double OCAD8FileImport::convertTemplateScale(double ocad_scale)
1552 {
1553 	return ocad_scale * 0.01;	// millimeters(on map) per pixel
1554 }
1555 
1556 
1557 // ### OCAD8FileExport ###
1558 
OCAD8FileExport(const QString & path,const Map * map,const MapView * view)1559 OCAD8FileExport::OCAD8FileExport(const QString& path, const Map* map, const MapView* view)
1560  : Exporter(path, map, view),
1561    uses_registration_color(false),
1562    file(nullptr)
1563 {
1564 	ocad_init();
1565 	encoding_1byte = QTextCodec::codecForName("Windows-1252");
1566 	encoding_2byte = QTextCodec::codecForName("UTF-16LE");
1567 
1568 	origin_point_object = new PointObject();
1569 }
1570 
~OCAD8FileExport()1571 OCAD8FileExport::~OCAD8FileExport()
1572 {
1573 	delete origin_point_object;
1574 }
1575 
exportImplementation()1576 bool OCAD8FileExport::exportImplementation()
1577 {
1578 	uses_registration_color = map->isColorUsedByASymbol(map->getRegistrationColor());
1579 	if (map->getNumColors() > (uses_registration_color ? 255 : 256))
1580 		throw FileFormatException(tr("The map contains more than 256 colors which is not supported by ocd version 8."));
1581 
1582 	// Create struct in memory
1583 	int err = ocad_file_new(&file);
1584 	if (err != 0) throw FileFormatException(tr("libocad returned %1").arg(err));
1585 
1586 	// Check for a necessary offset (and add related warnings early).
1587 	auto area_offset = calculateAreaOffset();
1588 
1589 	// Fill header struct
1590 	OCADFileHeader* header = file->header;
1591 	*(((u8*)&header->magic) + 0) = 0xAD;
1592 	*(((u8*)&header->magic) + 1) = 0x0C;
1593 	header->ftype = 2;
1594 	header->major = 8;
1595 	header->minor = 0;
1596 	if (map->getMapNotes().size() > 0)
1597 	{
1598 		header->infosize = map->getMapNotes().length() + 1;
1599 		ocad_file_reserve(file, header->infosize);
1600 		header->infopos = &file->buffer[file->size] - file->buffer;
1601 		convertCString(map->getMapNotes(), &file->buffer[file->size], header->infosize);
1602 		file->size += header->infosize;
1603 	}
1604 
1605 	// Fill setup struct
1606 	OCADSetup* setup = file->setup;
1607 	if (view)
1608 	{
1609 		setup->center = convertPoint(view->center() - area_offset);
1610 		setup->zoom = view->getZoom();
1611 	}
1612 	else
1613 		setup->zoom = 1;
1614 
1615 	// Scale and georeferencing parameters
1616 	const Georeferencing& georef = map->getGeoreferencing();
1617 	setup->scale = georef.getScaleDenominator();
1618 	const QPointF offset(georef.toProjectedCoords(area_offset));
1619 	setup->offsetx = offset.x();
1620 	setup->offsety = offset.y();
1621 	setup->angle = georef.getGrivation();
1622 
1623 	// TODO: print parameters
1624 
1625 	// Colors
1626 	int ocad_color_index = 0;
1627 	if (uses_registration_color)
1628 	{
1629 		addWarning(tr("Registration black is exported as a regular color."));
1630 
1631 		++file->header->ncolors;
1632 		OCADColor *ocad_color = ocad_color_at(file, ocad_color_index);
1633 		ocad_color->number = ocad_color_index;
1634 
1635 		const MapColor* color = Map::getRegistrationColor();
1636 		const MapColorCmyk& cmyk = color->getCmyk();
1637 		ocad_color->cyan = qRound(1 / 0.005f * cmyk.c);
1638 		ocad_color->magenta = qRound(1 / 0.005f * cmyk.m);
1639 		ocad_color->yellow = qRound(1 / 0.005f * cmyk.y);
1640 		ocad_color->black = qRound(1 / 0.005f * cmyk.k);
1641 		convertPascalString(QString::fromLatin1("Registration black"), ocad_color->name, 32); // not translated
1642 
1643 		++ocad_color_index;
1644 	}
1645 	for (int i = 0; i < map->getNumColors(); i++)
1646 	{
1647 		++file->header->ncolors;
1648 		OCADColor *ocad_color = ocad_color_at(file, ocad_color_index);
1649 		ocad_color->number = ocad_color_index;
1650 
1651 		const MapColor* color = map->getColor(i);
1652 		const MapColorCmyk& cmyk = color->getCmyk();
1653 		ocad_color->cyan = qRound(1 / 0.005f * cmyk.c);
1654 		ocad_color->magenta = qRound(1 / 0.005f * cmyk.m);
1655 		ocad_color->yellow = qRound(1 / 0.005f * cmyk.y);
1656 		ocad_color->black = qRound(1 / 0.005f * cmyk.k);
1657 		convertPascalString(color->getName(), ocad_color->name, 32);
1658 
1659 		++ocad_color_index;
1660 	}
1661 
1662 	// Symbols
1663 	for (int i = 0; i < map->getNumSymbols(); ++i)
1664 	{
1665 		const Symbol* symbol = map->getSymbol(i);
1666 
1667 		s16 index = -1;
1668 		if (symbol->getType() == Symbol::Point)
1669 			index = exportPointSymbol(symbol->asPoint());
1670 		else if (symbol->getType() == Symbol::Line)
1671 			index = exportLineSymbol(symbol->asLine());
1672 		else if (symbol->getType() == Symbol::Area)
1673 			index = exportAreaSymbol(symbol->asArea());
1674 		else if (symbol->getType() == Symbol::Text)
1675 			index = exportTextSymbol(symbol->asText());
1676 		else if (symbol->getType() == Symbol::Combined)
1677 			; // This is done as a second pass to ensure that all dependencies are added to the symbol_index
1678 		else
1679 			Q_ASSERT(false);
1680 
1681 		if (index >= 0)
1682 		{
1683 			std::set<s16> number;
1684 			number.insert(index);
1685 			symbol_index.insert(symbol, number);
1686 		}
1687 	}
1688 
1689 	// Separate pass for combined symbols
1690 	for (int i = 0; i < map->getNumSymbols(); ++i)
1691 	{
1692 		const Symbol* symbol = map->getSymbol(i);
1693 		if (symbol->getType() == Symbol::Combined)
1694 			symbol_index.insert(symbol, exportCombinedSymbol(static_cast<const CombinedSymbol*>(symbol)));
1695 	}
1696 
1697 	// Objects
1698 	OCADObject* ocad_object = ocad_object_alloc(nullptr);
1699 	for (int l = 0; l < map->getNumParts(); ++l)
1700 	{
1701 		for (int o = 0; o < map->getPart(l)->getNumObjects(); ++o)
1702 		{
1703 			memset(ocad_object, 0, sizeof(OCADObject) - sizeof(OCADPoint) + 8 * (ocad_object->npts + ocad_object->ntext));
1704 			Object* object = map->getPart(l)->getObject(o);
1705 			std::unique_ptr<Object> duplicate;
1706 			if (area_offset.nativeX() != 0 || area_offset.nativeY() != 0)
1707 			{
1708 				// Create a safely managed duplicate and move it as needed.
1709 				duplicate.reset(object->duplicate());
1710 				duplicate->move(-area_offset);
1711 				object = duplicate.get();
1712 			}
1713 			object->update();
1714 
1715 			// Fill some common entries of object struct
1716 			OCADPoint* coord_buffer = ocad_object->pts;
1717 			if (object->getType() != Object::Text)
1718 				ocad_object->npts = exportCoordinates(object->getRawCoordinateVector(), &coord_buffer, object->getSymbol());
1719 			else
1720 				ocad_object->npts = exportTextCoordinates(static_cast<TextObject*>(object), &coord_buffer);
1721 
1722 			if (object->getType() == Object::Point)
1723 			{
1724 				PointObject* point = static_cast<PointObject*>(object);
1725 				ocad_object->angle = convertRotation(point->getRotation());
1726 			}
1727 			else if (object->getType() == Object::Path)
1728 			{
1729 				if (object->getSymbol()->getType() == Symbol::Area)
1730 				{
1731 					PathObject* path = static_cast<PathObject*>(object);
1732 					// Known issue: In OCD format, pattern rotatability is all
1733 					// or nothing. In Mapper, it is an option per pattern.
1734 					if (path->getSymbol()->asArea()->hasRotatableFillPattern())
1735 						ocad_object->angle = convertRotation(path->getPatternRotation());
1736 					if (path->getPatternOrigin() != MapCoord(0, 0))
1737 						addWarning(tr("Unable to export fill pattern shift for an area object"));
1738 				}
1739 			}
1740 			else if (object->getType() == Object::Text)
1741 			{
1742 				TextObject* text = static_cast<TextObject*>(object);
1743 				ocad_object->unicode = 1;
1744 				ocad_object->angle = convertRotation(text->getRotation());
1745 				int num_letters = convertWideCString(text->getText(), (unsigned char*)coord_buffer, 8 * (OCAD_MAX_OBJECT_PTS - ocad_object->npts));
1746 				ocad_object->ntext = qCeil(num_letters / 4.0f);
1747 			}
1748 
1749 			// Insert an object into the map for every symbol contained in the symbol_index
1750 			std::set<s16> index_set;
1751 			if (symbol_index.contains(object->getSymbol()))
1752 				index_set = symbol_index[object->getSymbol()];
1753 			else
1754 				index_set.insert(-1);	// export as undefined symbol
1755 
1756 			for (const auto index : index_set)
1757 			{
1758 				s16 index_to_use = index;
1759 
1760 				// For text objects, check if we have to change / create a new text symbol because of the formatting
1761 				if (object->getType() == Object::Text && symbol_index.contains(object->getSymbol()))
1762 				{
1763 					TextObject* text_object = static_cast<TextObject*>(object);
1764 					const TextSymbol* text_symbol = static_cast<const TextSymbol*>(object->getSymbol());
1765 					if (!text_format_map.contains(text_symbol))
1766 					{
1767 						// Adjust the formatting in the first created symbol to this object
1768 						OCADTextSymbol* ocad_text_symbol = (OCADTextSymbol*)ocad_symbol(file, index);
1769 						setTextSymbolFormatting(ocad_text_symbol, text_object);
1770 
1771 						TextFormatList new_list;
1772 						new_list.push_back(std::make_pair(text_object->getHorizontalAlignment(), index));
1773 						text_format_map.insert(text_symbol, new_list);
1774 					}
1775 					else
1776 					{
1777 						// Check if this formatting has already been created as symbol.
1778 						// If yes, use this symbol, else create a new symbol
1779 						TextFormatList& format_list = text_format_map[text_symbol];
1780 						bool found = false;
1781 						for (size_t i = 0, end = format_list.size(); i < end; ++i)
1782 						{
1783 							if (format_list[i].first == text_object->getHorizontalAlignment())
1784 							{
1785 								index_to_use = format_list[i].second;
1786 								found = true;
1787 								break;
1788 							}
1789 						}
1790 						if (!found)
1791 						{
1792 							// Copy the symbol and adjust the formatting
1793 							// TODO: insert these symbols directly after the original symbols
1794 							OCADTextSymbol* ocad_text_symbol = (OCADTextSymbol*)ocad_symbol(file, index);
1795 							OCADTextSymbol* new_symbol = (OCADTextSymbol*)ocad_symbol_new(file, ocad_text_symbol->size);
1796 							// Get the pointer to the first symbol again as it might have changed during ocad_symbol_new()
1797 							ocad_text_symbol = (OCADTextSymbol*)ocad_symbol(file, index);
1798 
1799 							memcpy(new_symbol, ocad_text_symbol, ocad_text_symbol->size);
1800 							setTextSymbolFormatting(new_symbol, text_object);
1801 
1802 							// Give the new symbol a unique number
1803 							while (symbol_numbers.find(new_symbol->number) != symbol_numbers.end())
1804 								++new_symbol->number;
1805 							symbol_numbers.insert(new_symbol->number);
1806 							index_to_use = new_symbol->number;
1807 
1808 							// Store packed new_symbol->number in separate variable first,
1809 							// otherwise when compiling for Android this causes the error:
1810 							// cannot bind packed field 'new_symbol->_OCADTextSymbol::number' to 'short int&'
1811 							s16 new_symbol_number = new_symbol->number;
1812 							format_list.push_back(std::make_pair(text_object->getHorizontalAlignment(), new_symbol_number));
1813 						}
1814 					}
1815 				}
1816 
1817 				ocad_object->symbol = index_to_use;
1818 				if (object->getType() == Object::Point)
1819 					ocad_object->type = 1;
1820 				else if (object->getType() == Object::Path)
1821 				{
1822 					OCADSymbol* ocad_sym = ocad_symbol(file, index_to_use);
1823 					if (!ocad_sym)
1824 						ocad_object->type = 2;	// This case is for undefined lines; TODO: make another case for undefined areas, as soon as they are implemented
1825 					else if (ocad_sym->type == 2)
1826 						ocad_object->type = 2;	// Line
1827 					else //if (ocad_symbol->type == 3)
1828 						ocad_object->type = 3;	// Area
1829 				}
1830 				else if (object->getType() == Object::Text)
1831 				{
1832 					TextObject* text_object = static_cast<TextObject*>(object);
1833 					if (text_object->hasSingleAnchor())
1834 						ocad_object->type = 4;
1835 					else
1836 						ocad_object->type = 5;
1837 				}
1838 
1839 				OCADObjectEntry* entry;
1840 				ocad_object_add(file, ocad_object, &entry);
1841 				// This is done internally by libocad (in a slightly more imprecise way using the extent specified in the symbol)
1842 				//entry->rect.min = convertPoint(MapCoord(object->getExtent().topLeft()));
1843 				//entry->rect.max = convertPoint(MapCoord(object->getExtent().bottomRight()));
1844 				entry->npts = ocad_object->npts + ocad_object->ntext;
1845 				//entry->symbol = index_to_use;
1846 			}
1847 		}
1848 	}
1849 
1850 	// Templates
1851 	for (int i = map->getNumTemplates() - 1; i >= 0; --i)
1852 	{
1853 		const Template* temp = map->getTemplate(i);
1854 
1855 		QString template_path = temp->getTemplatePath();
1856 
1857 		auto supported_by_ocd = false;
1858 		if (qstrcmp(temp->getTemplateType(), "TemplateImage") == 0)
1859 		{
1860 			supported_by_ocd = true;
1861 
1862 			if (temp->isTemplateGeoreferenced())
1863 			{
1864 				if (temp->getTemplateState() == Template::Unloaded)
1865 				{
1866 					// Try to load the template, so that the positioning gets set.
1867 					const_cast<Template*>(temp)->loadTemplateFile(false);
1868 				}
1869 
1870 				if (temp->getTemplateState() != Template::Loaded)
1871 				{
1872 					addWarning(tr("Unable to save correct position of missing template: \"%1\"")
1873 					           .arg(temp->getTemplateFilename()));
1874 				}
1875 			}
1876 		}
1877 		else if (QFileInfo(template_path).suffix().compare(QLatin1String("ocd"), Qt::CaseInsensitive) == 0)
1878 		{
1879 			supported_by_ocd = true;
1880 		}
1881 
1882 		if (supported_by_ocd)
1883 		{
1884 			// FIXME: export template view parameters
1885 
1886 			double a = temp->getTemplateRotation() * 180 / M_PI;
1887 			int d = 0;
1888 			int o = 0;
1889 			int p = 0;
1890 			int s = 1;	// enabled
1891 			int t = 0;
1892 			OCADPoint pos = convertPoint(temp->getTemplateX()-area_offset.nativeX(), temp->getTemplateY()-area_offset.nativeY());
1893 			int x = pos.x >> 8;
1894 			int y = pos.y >> 8;
1895 			double u = convertTemplateScale(temp->getTemplateScaleX());
1896 			double v = convertTemplateScale(temp->getTemplateScaleY());
1897 
1898 			template_path.replace(QLatin1Char('/'), QLatin1Char('\\'));
1899 
1900 			QString string;
1901 			string.sprintf("\ts%d\tx%d\ty%d\ta%f\tu%f\tv%f\td%d\tp%d\tt%d\to%d",
1902 				s, x, y, a, u, v, d, p, t, o
1903 			);
1904 			string.prepend(template_path);
1905 
1906 			OCADStringEntry* entry = ocad_string_entry_new(file, string.length() + 1);
1907 			entry->type = 8;
1908 			convertCString(string, file->buffer + entry->ptr, entry->size);
1909 		}
1910 		else
1911 		{
1912 			addWarning(tr("Unable to export template: file type of \"%1\" is not supported yet").arg(temp->getTemplateFilename()));
1913 		}
1914 	}
1915 
1916 	device()->write((char*)file->buffer, file->size);
1917 
1918 	ocad_file_close(file);
1919 	return true;
1920 }
1921 
1922 
calculateAreaOffset()1923 MapCoord OCAD8FileExport::calculateAreaOffset()
1924 {
1925 	auto area_offset = QPointF{};
1926 
1927 	// Attention: When changing ocd_bounds, update the warning messages, too.
1928 	auto ocd_bounds = QRectF{QPointF{-2000, -2000}, QPointF{2000, 2000}};
1929 	auto objects_extent = map->calculateExtent();
1930 	if (!ocd_bounds.contains(objects_extent))
1931 	{
1932 		if (objects_extent.width() < ocd_bounds.width()
1933 		    && objects_extent.height() < ocd_bounds.height())
1934 		{
1935 			// The extent fits into the limited area.
1936 			addWarning(tr("Coordinates are adjusted to fit into the OCAD 8 drawing area (-2 m ... 2 m)."));
1937 			area_offset = objects_extent.center();
1938 		}
1939 		else
1940 		{
1941 			// The extent is too wide to fit.
1942 
1943 			// Only move the objects if they are completely outside the drawing area.
1944 			// This avoids repeated moves on open/save/close cycles.
1945 			if (!objects_extent.intersects(ocd_bounds))
1946 			{
1947 				addWarning(tr("Coordinates are adjusted to fit into the OCAD 8 drawing area (-2 m ... 2 m)."));
1948 				std::size_t count = 0;
1949 				auto calculate_average_center = [&area_offset, &count](const Object* object)
1950 				{
1951 					area_offset *= qreal(count)/qreal(count+1);
1952 					++count;
1953 					area_offset += object->getExtent().center() / count;
1954 				};
1955 				map->applyOnAllObjects(calculate_average_center);
1956 			}
1957 
1958 			addWarning(tr("Some coordinates remain outside of the OCAD 8 drawing area."
1959 			              " They might be unreachable in OCAD."));
1960 		}
1961 
1962 		if (area_offset.manhattanLength() > 0)
1963 		{
1964 			// Round offset to 100 m in projected coordinates, to avoid crude grid offset.
1965 			constexpr auto unit = 100;
1966 			auto projected_offset = map->getGeoreferencing().toProjectedCoords(MapCoordF(area_offset));
1967 			projected_offset.rx() = qreal(qRound(projected_offset.x()/unit)) * unit;
1968 			projected_offset.ry() = qreal(qRound(projected_offset.y()/unit)) * unit;
1969 			area_offset = map->getGeoreferencing().toMapCoordF(projected_offset);
1970 		}
1971 	}
1972 
1973 	return MapCoord{area_offset};
1974 }
1975 
1976 
exportCommonSymbolFields(const Symbol * symbol,OCADSymbol * ocad_symbol,int size)1977 void OCAD8FileExport::exportCommonSymbolFields(const Symbol* symbol, OCADSymbol* ocad_symbol, int size)
1978 {
1979 	ocad_symbol->size = (s16)size;
1980 	convertPascalString(symbol->getPlainTextName(), ocad_symbol->name, 32);
1981 	ocad_symbol->number = symbol->getNumberComponent(0) * 10;
1982 	if (symbol->getNumberComponent(1) >= 0)
1983 		ocad_symbol->number += (symbol->getNumberComponent(1) % 10);
1984 	// Symbol number 0.0 is not valid
1985 	if (ocad_symbol->number == 0)
1986 		ocad_symbol->number = 1;
1987 	// Ensure uniqueness of the symbol number
1988 	while (symbol_numbers.find(ocad_symbol->number) != symbol_numbers.end())
1989 		++ocad_symbol->number;
1990 	symbol_numbers.insert(ocad_symbol->number);
1991 
1992 	if (symbol->isProtected())
1993 		ocad_symbol->status |= 1;
1994 	if (symbol->isHidden())
1995 		ocad_symbol->status |= 2;
1996 
1997 	// Set of used colors
1998 	u8 bitmask = 1;
1999 	u8* bitpos = ocad_symbol->colors;
2000 	for (int c = 0; c < map->getNumColors(); ++c)
2001 	{
2002 		if (symbol->containsColor(map->getColor(c)))
2003 			*bitpos |= bitmask;
2004 
2005 		bitmask = bitmask << 1;
2006 		if (bitmask == 0)
2007 		{
2008 			bitmask = 1;
2009 			++bitpos;
2010 		}
2011 	}
2012 
2013 	exportSymbolIcon(symbol, ocad_symbol->icon);
2014 }
2015 
exportSymbolIcon(const Symbol * symbol,u8 ocad_icon[])2016 void OCAD8FileExport::exportSymbolIcon(const Symbol* symbol, u8 ocad_icon[])
2017 {
2018 	// Icon: 22x22 with 4 bit color code, origin at bottom left
2019 	constexpr int icon_size = 22;
2020 	QImage image = symbol->createIcon(*map, icon_size, false)
2021 	               .convertToFormat(QImage::Format_ARGB32_Premultiplied);
2022 
2023 	auto process_pixel = [&image](int x, int y)->int
2024 	{
2025 		// Apply premultiplied pixel on white background
2026 		auto premultiplied = image.pixel(x, y);
2027 		auto alpha = qAlpha(premultiplied);
2028 		auto r = 255 - alpha + qRed(premultiplied);
2029 		auto g = 255 - alpha + qGreen(premultiplied);
2030 		auto b = 255 - alpha + qBlue(premultiplied);
2031 		auto pixel = qRgb(r, g, b);
2032 
2033 		// Ordered dithering 2x2 threshold matrix, adjusted for o-map halftones
2034 		static int threshold[4] = { 24, 192, 136, 80 };
2035 		auto palette_color = getOcadColor(pixel);
2036 		switch (palette_color)
2037 		{
2038 		case 0:
2039 			// Black to gray (50%)
2040 			return  qGray(pixel) < 128-threshold[(x%2 + 2*(y%2))]/2 ? 0 : 7;
2041 
2042 		case 7:
2043 			// Gray (50%) to light gray
2044 			return  qGray(pixel) < 192-threshold[(x%2 + 2*(y%2))]/4 ? 7 : 8;
2045 
2046 		case 8:
2047 			// Light gray to white
2048 			return  qGray(pixel) < 256-threshold[(x%2 + 2*(y%2))]/4 ? 8 : 15;
2049 
2050 		case 15:
2051 			// Pure white
2052 			return palette_color;
2053 
2054 		default:
2055 			// Color to white
2056 			return  QColor(pixel).saturation() >= threshold[(x%2 + 2*(y%2))] ? palette_color : 15;
2057 		}
2058 	};
2059 
2060 	for (int y = icon_size - 1; y >= 0; --y)
2061 	{
2062 		for (int x = 0; x < icon_size; x += 2)
2063 		{
2064 			auto first = process_pixel(x, y);
2065 			auto second = process_pixel(x+1, y);
2066 			*(ocad_icon++) = u8((first << 4) + second);
2067 		}
2068 		ocad_icon++;
2069 	}
2070 }
2071 
getPatternSize(const PointSymbol * point)2072 int OCAD8FileExport::getPatternSize(const PointSymbol* point)
2073 {
2074 	if (!point)
2075 		return 0;
2076 
2077 	int npts = 0;
2078 	for (int i = 0; i < point->getNumElements(); ++i)
2079 	{
2080 		int factor = 1;
2081 		if (point->getElementSymbol(i)->getType() == Symbol::Point)
2082 		{
2083 			factor = 0;
2084 			const PointSymbol* point_symbol = static_cast<const PointSymbol*>(point->getElementSymbol(i));
2085 			if (point_symbol->getInnerRadius() > 0 && point_symbol->getInnerColor())
2086 				++factor;
2087 			if (point_symbol->getOuterWidth() > 0 && point_symbol->getOuterColor())
2088 				++factor;
2089 		}
2090 		npts += factor * (2 + point->getElementObject(i)->getRawCoordinateVector().size());
2091 	}
2092 	if (point->getInnerRadius() > 0 && point->getInnerColor())
2093 		npts += 2 + 1;
2094 	if (point->getOuterWidth() > 0 && point->getOuterColor())
2095 		npts += 2 + 1;
2096 
2097 	return npts * sizeof(OCADPoint);
2098 }
2099 
exportPattern(const PointSymbol * point,OCADPoint ** buffer)2100 s16 OCAD8FileExport::exportPattern(const PointSymbol* point, OCADPoint** buffer)
2101 {
2102 	if (!point)
2103 		return 0;
2104 
2105 	s16 num_coords = exportSubPattern(origin_point_object, point, buffer);
2106 	for (int i = 0; i < point->getNumElements(); ++i)
2107 	{
2108 		num_coords += exportSubPattern(point->getElementObject(i), point->getElementSymbol(i), buffer);
2109 	}
2110 	return num_coords;
2111 }
2112 
exportSubPattern(const Object * object,const Symbol * symbol,OCADPoint ** buffer)2113 s16 OCAD8FileExport::exportSubPattern(const Object* object, const Symbol* symbol, OCADPoint** buffer)
2114 {
2115 	s16 num_coords = 0;
2116 	OCADSymbolElement* element = (OCADSymbolElement*)*buffer;
2117 
2118 	if (symbol->getType() == Symbol::Point)
2119 	{
2120 		const PointSymbol* point_symbol = static_cast<const PointSymbol*>(symbol);
2121 		if (point_symbol->getInnerRadius() > 0 && point_symbol->getInnerColor())
2122 		{
2123 			element->type = 4;
2124 			element->color = convertColor(point_symbol->getInnerColor());
2125 			element->diameter = convertSize(2 * point_symbol->getInnerRadius());
2126 			(*buffer) += 2;
2127 			element->npts = exportCoordinates(object->getRawCoordinateVector(), buffer, point_symbol);
2128 			num_coords += 2 + element->npts;
2129 		}
2130 		if (point_symbol->getOuterWidth() > 0 && point_symbol->getOuterColor())
2131 		{
2132 			element = (OCADSymbolElement*)*buffer;
2133 			element->type = 3;
2134 			element->color = convertColor(point_symbol->getOuterColor());
2135 			element->width = convertSize(point_symbol->getOuterWidth());
2136 			element->diameter = convertSize(2 * point_symbol->getInnerRadius() + 2 * point_symbol->getOuterWidth());
2137 			(*buffer) += 2;
2138 			element->npts = exportCoordinates(object->getRawCoordinateVector(), buffer, point_symbol);
2139 			num_coords += 2 + element->npts;
2140 		}
2141 	}
2142 	else if (symbol->getType() == Symbol::Line)
2143 	{
2144 		const LineSymbol* line_symbol = static_cast<const LineSymbol*>(symbol);
2145 		element->type = 1;
2146 		if (line_symbol->getCapStyle() == LineSymbol::RoundCap)
2147 			element->flags |= 1;
2148 		else if (line_symbol->getJoinStyle() == LineSymbol::MiterJoin)
2149 			element->flags |= 4;
2150 		element->color = convertColor(line_symbol->getColor());
2151 		element->width = convertSize(line_symbol->getLineWidth());
2152 		(*buffer) += 2;
2153 		element->npts = exportCoordinates(object->getRawCoordinateVector(), buffer, line_symbol);
2154 		num_coords += 2 + element->npts;
2155 	}
2156 	else if (symbol->getType() == Symbol::Area)
2157 	{
2158 		const AreaSymbol* area_symbol = static_cast<const AreaSymbol*>(symbol);
2159 		element->type = 2;
2160 		element->color = convertColor(area_symbol->getColor());
2161 		(*buffer) += 2;
2162 		element->npts = exportCoordinates(object->getRawCoordinateVector(), buffer, area_symbol);
2163 		num_coords += 2 + element->npts;
2164 	}
2165 	else
2166 		Q_ASSERT(false);
2167 	return num_coords;
2168 }
2169 
exportPointSymbol(const PointSymbol * point)2170 s16 OCAD8FileExport::exportPointSymbol(const PointSymbol* point)
2171 {
2172 	int data_size = (sizeof(OCADPointSymbol) - sizeof(OCADPoint)) + getPatternSize(point);
2173 	OCADPointSymbol* ocad_symbol = (OCADPointSymbol*)ocad_symbol_new(file, data_size);
2174 	exportCommonSymbolFields(point, (OCADSymbol*)ocad_symbol, data_size);
2175 
2176 	ocad_symbol->type = OCAD_POINT_SYMBOL;
2177 	ocad_symbol->extent = getPointSymbolExtent(point);
2178 	if (ocad_symbol->extent <= 0)
2179 		ocad_symbol->extent = 100;
2180 	if (point->isRotatable())
2181 		ocad_symbol->base_flags |= 1;
2182 	ocad_symbol->ngrp = (data_size - (sizeof(OCADPointSymbol) - sizeof(OCADPoint))) / 8;
2183 
2184 	OCADPoint* pattern_buffer = ocad_symbol->pts;
2185 	exportPattern(point, &pattern_buffer);
2186 	Q_ASSERT((u8*)ocad_symbol + data_size == (u8*)pattern_buffer);
2187 	return ocad_symbol->number;
2188 }
2189 
exportLineSymbol(const LineSymbol * line)2190 s16 OCAD8FileExport::exportLineSymbol(const LineSymbol* line)
2191 {
2192 	int data_size = (sizeof(OCADLineSymbol) - sizeof(OCADPoint)) +
2193 					getPatternSize(line->getStartSymbol()) +
2194 					getPatternSize(line->getEndSymbol()) +
2195 					getPatternSize(line->getMidSymbol()) +
2196 					getPatternSize(line->getDashSymbol());
2197 	OCADLineSymbol* ocad_symbol = (OCADLineSymbol*)ocad_symbol_new(file, data_size);
2198 	exportCommonSymbolFields(line, (OCADSymbol*)ocad_symbol, data_size);
2199 
2200 	// Basic settings
2201 	ocad_symbol->type = OCAD_LINE_SYMBOL;
2202 	s16 extent = convertSize(0.5f * line->getLineWidth());
2203 	if (line->hasBorder())
2204 		extent = qMax(extent, (s16)convertSize(0.5f * line->getLineWidth() + line->getBorder().shift + 0.5f * line->getBorder().width));
2205 	extent = qMax(extent, getPointSymbolExtent(line->getStartSymbol()));
2206 	extent = qMax(extent, getPointSymbolExtent(line->getEndSymbol()));
2207 	extent = qMax(extent, getPointSymbolExtent(line->getMidSymbol()));
2208 	extent = qMax(extent, getPointSymbolExtent(line->getDashSymbol()));
2209 	ocad_symbol->extent = extent;
2210 	ocad_symbol->color = convertColor(line->getColor());
2211 	if (line->getColor())
2212 		ocad_symbol->width = convertSize(line->getLineWidth());
2213 
2214 	// Cap and Join
2215 	if (line->getCapStyle() == LineSymbol::FlatCap && line->getJoinStyle() == LineSymbol::BevelJoin)
2216 		ocad_symbol->ends = 0;
2217 	else if (line->getCapStyle() == LineSymbol::RoundCap && line->getJoinStyle() == LineSymbol::RoundJoin)
2218 		ocad_symbol->ends = 1;
2219 	else if (line->getCapStyle() == LineSymbol::PointedCap && line->getJoinStyle() == LineSymbol::BevelJoin)
2220 		ocad_symbol->ends = 2;
2221 	else if (line->getCapStyle() == LineSymbol::PointedCap && line->getJoinStyle() == LineSymbol::RoundJoin)
2222 		ocad_symbol->ends = 3;
2223 	else if (line->getCapStyle() == LineSymbol::FlatCap && line->getJoinStyle() == LineSymbol::MiterJoin)
2224 		ocad_symbol->ends = 4;
2225 	else if (line->getCapStyle() == LineSymbol::PointedCap && line->getJoinStyle() == LineSymbol::MiterJoin)
2226 		ocad_symbol->ends = 6;
2227 	else
2228 	{
2229 		addWarning(tr("In line symbol \"%1\", cannot represent cap/join combination.").arg(line->getPlainTextName()));
2230 		// Decide based on the caps
2231 		if (line->getCapStyle() == LineSymbol::FlatCap)
2232 			ocad_symbol->ends = 0;
2233 		else if (line->getCapStyle() == LineSymbol::RoundCap)
2234 			ocad_symbol->ends = 1;
2235 		else if (line->getCapStyle() == LineSymbol::PointedCap)
2236 			ocad_symbol->ends = 3;
2237 		else if (line->getCapStyle() == LineSymbol::SquareCap)
2238 			ocad_symbol->ends = 0;
2239 	}
2240 
2241 	ocad_symbol->bdist = convertSize(line->startOffset());
2242 	ocad_symbol->edist = convertSize(line->endOffset());
2243 
2244 	// Dash pattern
2245 	if (line->isDashed())
2246 	{
2247 		if (line->getMidSymbol() && !line->getMidSymbol()->isEmpty())
2248 		{
2249 			if (line->getDashesInGroup() > 1)
2250 				addWarning(tr("In line symbol \"%1\", neglecting the dash grouping.").arg(line->getPlainTextName()));
2251 
2252 			ocad_symbol->len = convertSize(line->getDashLength() + line->getBreakLength());
2253 			ocad_symbol->elen = ocad_symbol->len / 2;
2254 			ocad_symbol->gap2 = convertSize(line->getBreakLength());
2255 		}
2256 		else
2257 		{
2258 			if (line->getDashesInGroup() > 1)
2259 			{
2260 				if (line->getDashesInGroup() > 2)
2261 					addWarning(tr("In line symbol \"%1\", the number of dashes in a group has been reduced to 2.").arg(line->getPlainTextName()));
2262 
2263 				ocad_symbol->len = convertSize(2 * line->getDashLength() + line->getInGroupBreakLength());
2264 				ocad_symbol->elen = convertSize(2 * line->getDashLength() + line->getInGroupBreakLength());
2265 				ocad_symbol->gap = convertSize(line->getBreakLength());
2266 				ocad_symbol->gap2 = convertSize(line->getInGroupBreakLength());
2267 				ocad_symbol->egap = ocad_symbol->gap2;
2268 			}
2269 			else
2270 			{
2271 				ocad_symbol->len = convertSize(line->getDashLength());
2272 				ocad_symbol->elen = ocad_symbol->len / (line->getHalfOuterDashes() ? 2 : 1);
2273 				ocad_symbol->gap = convertSize(line->getBreakLength());
2274 			}
2275 		}
2276 	}
2277 	else
2278 	{
2279 		ocad_symbol->len = convertSize(line->getSegmentLength());
2280 		ocad_symbol->elen = convertSize(line->getEndLength());
2281 	}
2282 
2283 	ocad_symbol->smin = line->getShowAtLeastOneSymbol() ? 0 : -1;
2284 
2285 	// Double line
2286 	if (line->hasBorder() && (line->getBorder().isVisible() || line->getRightBorder().isVisible()))
2287 	{
2288 		ocad_symbol->dwidth = convertSize(line->getLineWidth() - line->getBorder().width + 2 * line->getBorder().shift);
2289 		if (line->getBorder().dashed && !line->getRightBorder().dashed)
2290 			ocad_symbol->dmode = 2;
2291 		else
2292 			ocad_symbol->dmode = line->getBorder().dashed ? 3 : 1;
2293 		// ocad_symbol->dflags
2294 
2295 		ocad_symbol->lwidth = convertSize(line->getBorder().width);
2296 		ocad_symbol->rwidth = convertSize(line->getRightBorder().width);
2297 
2298 		ocad_symbol->lcolor = convertColor(line->getBorder().color);
2299 		ocad_symbol->rcolor = convertColor(line->getRightBorder().color);
2300 
2301 		if (line->getBorder().dashed)
2302 		{
2303 			ocad_symbol->dlen = convertSize(line->getBorder().dash_length);
2304 			ocad_symbol->dgap = convertSize(line->getBorder().break_length);
2305 		}
2306 		else if (line->getRightBorder().dashed)
2307 		{
2308 			ocad_symbol->dlen = convertSize(line->getRightBorder().dash_length);
2309 			ocad_symbol->dgap = convertSize(line->getRightBorder().break_length);
2310 		}
2311 
2312 		if (((line->getBorder().dashed && line->getRightBorder().dashed) &&
2313 				(line->getBorder().dash_length != line->getRightBorder().dash_length ||
2314 				line->getBorder().break_length != line->getRightBorder().break_length)) ||
2315 			(!line->getBorder().dashed && line->getRightBorder().dashed))
2316 		{
2317 			addWarning(tr("In line symbol \"%1\", cannot export the borders correctly.").arg(line->getPlainTextName()));
2318 		}
2319 	}
2320 
2321 	// Mid symbol
2322 	OCADPoint* pattern_buffer = ocad_symbol->pts;
2323 	ocad_symbol->smnpts = exportPattern(line->getMidSymbol(), &pattern_buffer);
2324 	ocad_symbol->snum = line->getMidSymbolsPerSpot();
2325 	ocad_symbol->sdist = convertSize(line->getMidSymbolDistance());
2326 
2327 	// No secondary symbol
2328 	ocad_symbol->ssnpts = 0;
2329 
2330 	// Export dash symbol as corner symbol
2331 	ocad_symbol->scnpts = exportPattern(line->getDashSymbol(), &pattern_buffer);
2332 
2333 	// Start symbol
2334 	ocad_symbol->sbnpts = exportPattern(line->getStartSymbol(), &pattern_buffer);
2335 
2336 	// End symbol
2337 	ocad_symbol->senpts = exportPattern(line->getEndSymbol(), &pattern_buffer);
2338 
2339 	Q_ASSERT((u8*)ocad_symbol + data_size == (u8*)pattern_buffer);
2340 	return ocad_symbol->number;
2341 }
2342 
exportAreaSymbol(const AreaSymbol * area)2343 s16 OCAD8FileExport::exportAreaSymbol(const AreaSymbol* area)
2344 {
2345 	int data_size = (sizeof(OCADAreaSymbol) - sizeof(OCADPoint));
2346 	for (int i = 0, end = area->getNumFillPatterns(); i < end; ++i)
2347 	{
2348 		if (area->getFillPattern(i).type == AreaSymbol::FillPattern::PointPattern)
2349 		{
2350 			data_size += getPatternSize(area->getFillPattern(i).point);
2351 			break;
2352 		}
2353 	}
2354 	OCADAreaSymbol* ocad_symbol = (OCADAreaSymbol*)ocad_symbol_new(file, data_size);
2355 	exportCommonSymbolFields(area, (OCADSymbol*)ocad_symbol, data_size);
2356 
2357 	// Basic settings
2358 	ocad_symbol->type = OCAD_AREA_SYMBOL;
2359 	ocad_symbol->extent = 0;
2360 	if (area->getColor())
2361 	{
2362 		ocad_symbol->fill = 1;
2363 		ocad_symbol->color = convertColor(area->getColor());
2364 	}
2365 
2366 	// Hatch
2367 	ocad_symbol->hmode = 0;
2368 	for (int i = 0, end = area->getNumFillPatterns(); i < end; ++i)
2369 	{
2370 		const AreaSymbol::FillPattern& pattern = area->getFillPattern(i);
2371 		if (pattern.type == AreaSymbol::FillPattern::LinePattern)
2372 		{
2373 			if ( (ocad_symbol->hmode == 1 && ocad_symbol->hcolor != convertColor(pattern.line_color)) ||
2374 			     ocad_symbol->hmode == 2 )
2375 			{
2376 				addWarning(tr("In area symbol \"%1\", skipping a fill pattern.").arg(area->getPlainTextName()));
2377 				continue;
2378 			}
2379 
2380 			if (pattern.rotatable())
2381 				ocad_symbol->base_flags |= 1;
2382 
2383 			++ocad_symbol->hmode;
2384 			if (ocad_symbol->hmode == 1)
2385 			{
2386 				ocad_symbol->hcolor = convertColor(pattern.line_color);
2387 				ocad_symbol->hwidth = convertSize(pattern.line_width);
2388 				ocad_symbol->hdist = convertSize(pattern.line_spacing - pattern.line_width);
2389 				ocad_symbol->hangle1 = convertRotation(pattern.angle);
2390 			}
2391 			else if (ocad_symbol->hmode == 2)
2392 			{
2393 				ocad_symbol->hwidth = (ocad_symbol->hwidth + convertSize(pattern.line_width)) / 2;
2394 				ocad_symbol->hdist = (ocad_symbol->hdist + convertSize(pattern.line_spacing - pattern.line_width)) / 2;
2395 				ocad_symbol->hangle2 = convertRotation(pattern.angle);
2396 			}
2397 		}
2398 	}
2399 
2400 	// Struct
2401 	PointSymbol* point_pattern = nullptr;
2402 	for (int i = 0, end = area->getNumFillPatterns(); i < end; ++i)
2403 	{
2404 		const AreaSymbol::FillPattern& pattern = area->getFillPattern(i);
2405 		if (pattern.type == AreaSymbol::FillPattern::PointPattern)
2406 		{
2407 			if (pattern.rotatable())
2408 				ocad_symbol->base_flags |= 1;
2409 
2410 			++ocad_symbol->pmode;
2411 			if (ocad_symbol->pmode == 1)
2412 			{
2413 				ocad_symbol->pwidth = convertSize(pattern.point_distance);
2414 				ocad_symbol->pheight = convertSize(pattern.line_spacing);
2415 				ocad_symbol->pangle = convertRotation(pattern.angle);
2416 				point_pattern = pattern.point;
2417 			}
2418 			else if (ocad_symbol->pmode == 2)
2419 			{
2420 				// NOTE: This is only a heuristic which works for the orienteering symbol sets, not a real conversion, which would be impossible in most cases.
2421 				//       There are no further checks done to find out if the conversion is applicable because with these checks, already a tiny (not noticeable) error
2422 				//       in the symbol definition would make it take the wrong choice.
2423 				addWarning(tr("In area symbol \"%1\", assuming a \"shifted rows\" point pattern. This might be correct as well as incorrect.").arg(area->getPlainTextName()));
2424 
2425 				if (pattern.line_offset != 0)
2426 					ocad_symbol->pheight /= 2;
2427 				else
2428 					ocad_symbol->pwidth /= 2;
2429 
2430 				break;
2431 			}
2432 		}
2433 	}
2434 
2435 	if (point_pattern)
2436 	{
2437 		OCADPoint* pattern_buffer = ocad_symbol->pts;
2438 		ocad_symbol->npts = exportPattern(point_pattern, &pattern_buffer);
2439 		Q_ASSERT((u8*)ocad_symbol + data_size == (u8*)pattern_buffer);
2440 	}
2441 	return ocad_symbol->number;
2442 }
2443 
exportTextSymbol(const TextSymbol * text)2444 s16 OCAD8FileExport::exportTextSymbol(const TextSymbol* text)
2445 {
2446 	int data_size = sizeof(OCADTextSymbol);
2447 	OCADTextSymbol* ocad_symbol = (OCADTextSymbol*)ocad_symbol_new(file, data_size);
2448 	exportCommonSymbolFields(text, (OCADSymbol*)ocad_symbol, data_size);
2449 
2450 	ocad_symbol->type = OCAD_TEXT_SYMBOL;
2451 	ocad_symbol->subtype = 1;
2452 	ocad_symbol->extent = 0;
2453 
2454 	convertPascalString(text->getFontFamily(), ocad_symbol->font, 32);
2455 	ocad_symbol->color = convertColor(text->getColor());
2456 	ocad_symbol->dpts = qRound(10 * text->getFontSize() / 25.4 * 72.0);
2457 	ocad_symbol->bold = text->isBold() ? 700 : 400;
2458 	ocad_symbol->italic = text->isItalic() ? 1 : 0;
2459 	//ocad_symbol->charset
2460 	ocad_symbol->cspace = convertSize(1000 * text->getCharacterSpacing());
2461 	if (ocad_symbol->cspace != 0)
2462 		addWarning(tr("In text symbol %1: custom character spacing is set, its implementation does not match OCAD's behavior yet").arg(text->getPlainTextName()));
2463 	ocad_symbol->wspace = 100;
2464 	ocad_symbol->halign = 0;	// Default value, we might have to change this or even create copies of this symbol with other alignments later
2465 	double absolute_line_spacing = text->getLineSpacing() * (text->getFontMetrics().lineSpacing() / text->calculateInternalScaling());
2466 	ocad_symbol->lspace = qRound(absolute_line_spacing / (text->getFontSize() * 0.01));
2467 	ocad_symbol->pspace = convertSize(1000 * text->getParagraphSpacing());
2468 	if (text->isUnderlined())
2469 		addWarning(tr("In text symbol %1: ignoring underlining").arg(text->getPlainTextName()));
2470 	if (text->usesKerning())
2471 		addWarning(tr("In text symbol %1: ignoring kerning").arg(text->getPlainTextName()));
2472 
2473 	ocad_symbol->under = text->hasLineBelow() ? 1 : 0;
2474 	ocad_symbol->ucolor = convertColor(text->getLineBelowColor());
2475 	ocad_symbol->uwidth = convertSize(1000 * text->getLineBelowWidth());
2476 	ocad_symbol->udist = convertSize(1000 * text->getLineBelowDistance());
2477 
2478 	ocad_symbol->ntabs = text->getNumCustomTabs();
2479 	for (int i = 0; i < qMin((s16)32, ocad_symbol->ntabs); ++i)
2480 		ocad_symbol->tab[i] = convertSize(text->getCustomTab(i));
2481 
2482 	if (text->getFramingMode() != TextSymbol::NoFraming && text->getFramingColor())
2483 	{
2484 		ocad_symbol->fcolor = convertColor(text->getFramingColor());
2485 		if (text->getFramingMode() == TextSymbol::ShadowFraming)
2486 		{
2487 			ocad_symbol->fmode = 1;
2488 			ocad_symbol->fdx = convertSize(text->getFramingShadowXOffset());
2489 			ocad_symbol->fdy = -1 * convertSize(text->getFramingShadowYOffset());
2490 		}
2491 		else if (text->getFramingMode() == TextSymbol::LineFraming)
2492 		{
2493 			ocad_symbol->fmode = 2;
2494 			ocad_symbol->fdpts = convertSize(text->getFramingLineHalfWidth());
2495 		}
2496 		else
2497 			Q_ASSERT(false);
2498 	}
2499 
2500 	return ocad_symbol->number;
2501 }
2502 
setTextSymbolFormatting(OCADTextSymbol * ocad_symbol,TextObject * formatting)2503 void OCAD8FileExport::setTextSymbolFormatting(OCADTextSymbol* ocad_symbol, TextObject* formatting)
2504 {
2505 	if (formatting->getHorizontalAlignment() == TextObject::AlignLeft)
2506 		ocad_symbol->halign = 0;
2507 	else if (formatting->getHorizontalAlignment() == TextObject::AlignHCenter)
2508 		ocad_symbol->halign = 1;
2509 	else if (formatting->getHorizontalAlignment() == TextObject::AlignRight)
2510 		ocad_symbol->halign = 2;
2511 }
2512 
exportCombinedSymbol(const CombinedSymbol * combination)2513 std::set< s16 > OCAD8FileExport::exportCombinedSymbol(const CombinedSymbol* combination)
2514 {
2515 	// Insert public parts
2516 	std::vector<bool> map_bitfield;
2517 	map_bitfield.assign(map->getNumSymbols(), false);
2518 	map_bitfield[map->findSymbolIndex(combination)] = true;
2519 	map->determineSymbolUseClosure(map_bitfield);
2520 
2521 	std::set<s16> result;
2522 	for (size_t i = 0, end = map_bitfield.size(); i < end; ++i)
2523 	{
2524 		if (map_bitfield[i] && symbol_index.contains(map->getSymbol(i)))
2525 		{
2526 			result.insert(symbol_index[map->getSymbol(i)].begin(),
2527 			              symbol_index[map->getSymbol(i)].end());
2528 		}
2529 	}
2530 
2531 	// Insert private parts
2532 	for (int i = 0; i < combination->getNumParts(); ++i)
2533 	{
2534 		if (combination->isPartPrivate(i))
2535 		{
2536 			const Symbol* part = combination->getPart(i);
2537 			int index = 0;
2538 			if (part->getType() == Symbol::Line)
2539 				index = exportLineSymbol(part->asLine());
2540 			else if (part->getType() == Symbol::Area)
2541 				index = exportAreaSymbol(part->asArea());
2542 			else
2543 				Q_ASSERT(false);
2544 			result.insert(index);
2545 		}
2546 	}
2547 
2548 	return result;
2549 }
2550 
exportCoordinates(const MapCoordVector & coords,OCADPoint ** buffer,const Symbol * symbol)2551 u16 OCAD8FileExport::exportCoordinates(const MapCoordVector& coords, OCADPoint** buffer, const Symbol* symbol)
2552 {
2553 	s16 num_points = 0;
2554 	bool curve_start = false;
2555 	bool hole_point = false;
2556 	bool curve_continue = false;
2557 	for (size_t i = 0, end = coords.size(); i < end; ++i)
2558 	{
2559 		const MapCoord& point = coords[i];
2560 		OCADPoint p = convertPoint(point);
2561 		if (point.isDashPoint())
2562 		{
2563 			if (!symbol || symbol->getType() != Symbol::Line)
2564 				p.y |= PY_CORNER;
2565 			else
2566 			{
2567 				const LineSymbol* line_symbol = static_cast<const LineSymbol*>(symbol);
2568 				if ((line_symbol->getDashSymbol() == nullptr || line_symbol->getDashSymbol()->isEmpty()) && line_symbol->isDashed())
2569 					p.y |= PY_DASH;
2570 				else
2571 					p.y |= PY_CORNER;
2572 			}
2573 		}
2574 		if (curve_start)
2575 			p.x |= PX_CTL1;
2576 		if (hole_point)
2577 			p.y |= PY_HOLE;
2578 		if (curve_continue)
2579 			p.x |= PX_CTL2;
2580 
2581 		curve_continue = curve_start;
2582 		curve_start = point.isCurveStart();
2583 		hole_point = point.isHolePoint();
2584 
2585 		**buffer = p;
2586 		++(*buffer);
2587 		++num_points;
2588 	}
2589 	return num_points;
2590 }
2591 
exportTextCoordinates(TextObject * object,OCADPoint ** buffer)2592 u16 OCAD8FileExport::exportTextCoordinates(TextObject* object, OCADPoint** buffer)
2593 {
2594 	if (object->getNumLines() == 0)
2595 		return 0;
2596 
2597 	QTransform text_to_map = object->calcTextToMapTransform();
2598 	QTransform map_to_text = object->calcMapToTextTransform();
2599 
2600 	if (object->hasSingleAnchor())
2601 	{
2602 		// Create 5 coordinates:
2603 		// 0 - baseline anchor point
2604 		// 1 - bottom left
2605 		// 2 - bottom right
2606 		// 3 - top right
2607 		// 4 - top left
2608 
2609 		QPointF anchor = QPointF(object->getAnchorCoordF());
2610 		QPointF anchor_text = map_to_text.map(anchor);
2611 
2612 		TextObjectLineInfo* line0 = object->getLineInfo(0);
2613 		**buffer = convertPoint(MapCoord(text_to_map.map(QPointF(anchor_text.x(), line0->line_y))));
2614 		++(*buffer);
2615 
2616 		QRectF bounding_box_text;
2617 		for (int i = 0; i < object->getNumLines(); ++i)
2618 		{
2619 			TextObjectLineInfo* info = object->getLineInfo(i);
2620 			rectIncludeSafe(bounding_box_text, QPointF(info->line_x, info->line_y - info->ascent));
2621 			rectIncludeSafe(bounding_box_text, QPointF(info->line_x + info->width, info->line_y + info->descent));
2622 		}
2623 
2624 		**buffer = convertPoint(MapCoord(text_to_map.map(bounding_box_text.bottomLeft())));
2625 		++(*buffer);
2626 		**buffer = convertPoint(MapCoord(text_to_map.map(bounding_box_text.bottomRight())));
2627 		++(*buffer);
2628 		**buffer = convertPoint(MapCoord(text_to_map.map(bounding_box_text.topRight())));
2629 		++(*buffer);
2630 		**buffer = convertPoint(MapCoord(text_to_map.map(bounding_box_text.topLeft())));
2631 		++(*buffer);
2632 
2633 		return 5;
2634 	}
2635 	else
2636 	{
2637 		// As OCD 8 only supports Top alignment, we have to replace the top box coordinates by the top coordinates of the first line
2638 		const TextSymbol* text_symbol = static_cast<const TextSymbol*>(object->getSymbol());
2639 		QFontMetricsF metrics = text_symbol->getFontMetrics();
2640 		double internal_scaling = text_symbol->calculateInternalScaling();
2641 		TextObjectLineInfo* line0 = object->getLineInfo(0);
2642 
2643 		double new_top = (object->getVerticalAlignment() == TextObject::AlignTop) ? (-object->getBoxHeight() / 2) : ((line0->line_y - line0->ascent) / internal_scaling);
2644 		// Account for extra internal leading
2645 		double top_adjust = -text_symbol->getFontSize() + (metrics.ascent() + metrics.descent() + 0.5) / internal_scaling;
2646 		new_top = new_top - top_adjust;
2647 
2648 		QTransform transform;
2649 		transform.rotate(-object->getRotation() * 180 / M_PI);
2650 		**buffer = convertPoint(MapCoord(transform.map(QPointF(-object->getBoxWidth() / 2, object->getBoxHeight() / 2)) + object->getAnchorCoordF()));
2651 		++(*buffer);
2652 		**buffer = convertPoint(MapCoord(transform.map(QPointF(object->getBoxWidth() / 2, object->getBoxHeight() / 2)) + object->getAnchorCoordF()));
2653 		++(*buffer);
2654 		**buffer = convertPoint(MapCoord(transform.map(QPointF(object->getBoxWidth() / 2, new_top)) + object->getAnchorCoordF()));
2655 		++(*buffer);
2656 		**buffer = convertPoint(MapCoord(transform.map(QPointF(-object->getBoxWidth() / 2, new_top)) + object->getAnchorCoordF()));
2657 		++(*buffer);
2658 
2659 		return 4;
2660 	}
2661 }
2662 
2663 // static
getOcadColor(QRgb rgb)2664 int OCAD8FileExport::getOcadColor(QRgb rgb)
2665 {
2666 	static const QColor ocad_colors[16] = {
2667 		QColor(  0,   0,   0).toHsv(),
2668 		QColor(128,   0,   0).toHsv(),
2669 		QColor(0,   128,   0).toHsv(),
2670 		QColor(128, 128,   0).toHsv(),
2671 		QColor(  0,   0, 128).toHsv(),
2672 		QColor(128,   0, 128).toHsv(),
2673 		QColor(  0, 128, 128).toHsv(),
2674 		QColor(128, 128, 128).toHsv(),
2675 		QColor(192, 192, 192).toHsv(),
2676 		QColor(255,   0,   0).toHsv(),
2677 		QColor(  0, 255,   0).toHsv(),
2678 		QColor(255, 255,   0).toHsv(),
2679 		QColor(  0,   0, 255).toHsv(),
2680 		QColor(255,   0, 255).toHsv(),
2681 		QColor(  0, 255, 255).toHsv(),
2682 		QColor(255, 255, 255).toHsv()
2683 	};
2684 
2685 	Q_ASSERT(qAlpha(rgb) == 255);
2686 
2687 	// Quick return for frequent values
2688 	if (rgb == qRgb(255, 255, 255))
2689 		return 15;
2690 	else if (rgb == qRgb(0, 0, 0))
2691 		return 0;
2692 
2693 	QColor color = QColor(rgb).toHsv();
2694 	if (color.hue() == -1 || color.saturation() < 32)
2695 	{
2696 		auto gray = qGray(rgb);  // qGray is used for dithering
2697 		if (gray >= 192)
2698 			return 8;
2699 		if (gray >= 128)
2700 			return 7;
2701 		return 0;
2702 	}
2703 
2704 	int best_index = 0;
2705 	auto best_distance = std::numeric_limits<qreal>::max();
2706 	for (auto i : { 1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14 })
2707 	{
2708 		// True color
2709 		int hue_dist = qAbs(color.hue() - ocad_colors[i].hue());
2710 		hue_dist = qMin(hue_dist, 360 - hue_dist);
2711 		auto distance = qPow(hue_dist, 2)
2712 		                + 0.1 * qPow(color.saturation() - ocad_colors[i].saturation(), 2)
2713 		                + 0.1 * qPow(color.value() - ocad_colors[i].value(), 2);
2714 
2715 		// (Too much) manual tweaking for orienteering colors
2716 		if (i == 1)
2717 			distance *= 1.5;	// Dark red
2718 		else if (i == 3)
2719 			distance *= 2;		// Olive
2720 		else if (i == 11)
2721 			distance *= 2;		// Yellow
2722 		else if (i == 9)
2723 			distance *= 3;		// Red is unlikely
2724 
2725 		if (distance < best_distance)
2726 		{
2727 			best_distance = distance;
2728 			best_index = i;
2729 		}
2730 	}
2731 	return best_index;
2732 }
2733 
getPointSymbolExtent(const PointSymbol * symbol)2734 s16 OCAD8FileExport::getPointSymbolExtent(const PointSymbol* symbol)
2735 {
2736 	if (!symbol)
2737 		return 0;
2738 	QRectF extent;
2739 	for (int i = 0; i < symbol->getNumElements(); ++i)
2740 	{
2741 		QScopedPointer<Object> object(symbol->getElementObject(i)->duplicate());
2742 		object->setSymbol(symbol->getElementSymbol(i), true);
2743 		object->update();
2744 		rectIncludeSafe(extent, object->getExtent());
2745 		object->clearRenderables();
2746 	}
2747 	float float_extent = 0.5f * qMax(extent.width(), extent.height());
2748 	if (symbol->getInnerColor())
2749 		float_extent = qMax(float_extent, 0.001f * symbol->getInnerRadius());
2750 	if (symbol->getOuterColor())
2751 		float_extent = qMax(float_extent, 0.001f * (symbol->getInnerRadius() + symbol->getOuterWidth()));
2752 	return convertSize(1000 * float_extent);
2753 }
2754 
convertPascalString(const QString & text,char * buffer,int buffer_size)2755 void OCAD8FileExport::convertPascalString(const QString& text, char* buffer, int buffer_size)
2756 {
2757 	Q_ASSERT(buffer_size <= 256);		// not possible to store a bigger length in the first byte
2758 	int max_size = buffer_size - 1;
2759 
2760 	if (text.length() > max_size)
2761 		addStringTruncationWarning(text, max_size);
2762 
2763 	QByteArray data = encoding_1byte->fromUnicode(text);
2764 	int min_size = qMin(text.length(), max_size);
2765 	*((unsigned char *)buffer) = min_size;
2766 	memcpy(buffer + 1, data.data(), min_size);
2767 }
2768 
convertCString(const QString & text,unsigned char * buffer,int buffer_size)2769 void OCAD8FileExport::convertCString(const QString& text, unsigned char* buffer, int buffer_size)
2770 {
2771 	if (text.length() + 1 > buffer_size)
2772 		addStringTruncationWarning(text, buffer_size - 1);
2773 
2774 	QByteArray data = encoding_1byte->fromUnicode(text);
2775 	int min_size = qMin(buffer_size - 1, data.length());
2776 	memcpy(buffer, data.data(), min_size);
2777 	buffer[min_size] = 0;
2778 }
2779 
convertWideCString(const QString & text,unsigned char * buffer,int buffer_size)2780 int OCAD8FileExport::convertWideCString(const QString& text, unsigned char* buffer, int buffer_size)
2781 {
2782 	// Convert text to Windows-OCAD format:
2783 	// - if it starts with a newline, add another
2784 	// - convert \n to \r\n
2785 	QString exported_text;
2786 	if (text.startsWith(QLatin1Char('\n')))
2787 		exported_text = QLatin1Char('\n') + text;
2788 	else
2789 		exported_text = text;
2790 	exported_text.replace(QLatin1Char('\n'), QLatin1String("\r\n"));
2791 
2792 	if (2 * (exported_text.length() + 1) > buffer_size)
2793 		addStringTruncationWarning(exported_text, buffer_size - 1);
2794 
2795 	// Do not add a byte order mark by using QTextCodec::IgnoreHeader
2796 	QTextEncoder* encoder = encoding_2byte->makeEncoder(QTextCodec::IgnoreHeader);
2797 	QByteArray data = encoder->fromUnicode(exported_text);
2798 	delete encoder;
2799 
2800 	int min_size = qMin(buffer_size - 2, data.length());
2801 	memcpy(buffer, data.data(), min_size);
2802 	buffer[min_size] = 0;
2803 	buffer[min_size + 1] = 0;
2804 	return min_size + 2;
2805 }
2806 
convertRotation(float angle)2807 int OCAD8FileExport::convertRotation(float angle)
2808 {
2809 	return qRound(10 * (angle * 180 / M_PI));
2810 }
2811 
2812 namespace
2813 {
convertPointMember(s32 value)2814 	constexpr s32 convertPointMember(s32 value)
2815 	{
2816 		return (value < -5) ? s32(0x80000000u | ((0x7fffffu & u32((value-4)/10)) << 8)) : s32((0x7fffffu & u32((value+5)/10)) << 8);
2817 	}
2818 
2819 	// convertPointMember() shall round half up.
2820 	Q_STATIC_ASSERT(convertPointMember(-16) == s32(0xfffffe00u)); // __ down __
2821 	Q_STATIC_ASSERT(convertPointMember(-15) == s32(0xffffff00u)); //     up
2822 	Q_STATIC_ASSERT(convertPointMember( -6) == s32(0xffffff00u)); // __ down __
2823 	Q_STATIC_ASSERT(convertPointMember( -5) == s32(0x00000000u)); //     up
2824 	Q_STATIC_ASSERT(convertPointMember( -1) == s32(0x00000000u)); //     up
2825 	Q_STATIC_ASSERT(convertPointMember(  0) == s32(0x00000000u)); //  unchanged
2826 	Q_STATIC_ASSERT(convertPointMember( +1) == s32(0x00000000u)); //    down
2827 	Q_STATIC_ASSERT(convertPointMember( +4) == s32(0x00000000u)); // __ down __
2828 	Q_STATIC_ASSERT(convertPointMember( +5) == s32(0x00000100u)); //     up
2829 	Q_STATIC_ASSERT(convertPointMember(+14) == s32(0x00000100u)); // __ down __
2830 	Q_STATIC_ASSERT(convertPointMember(+15) == s32(0x00000200u)); //     up
2831 
2832 #ifdef MAPPER_DEVELOPMENT_BUILD
2833 	namespace broken
2834 	{
2835 		// Previous, broken implementation (#749)
2836 		// Left here for reference.
convertPointMember(s32 value)2837 		constexpr s32 convertPointMember(s32 value)
2838 		{
2839 			return (value < 0) ? (0x80000000 | ((0x7fffffu & ((value-5)/10)) << 8)) : ((0x7fffffu & ((value+5)/10)) << 8);
2840 		}
2841 	}
2842 	// Actual behaviour of the broken implementation
2843 	Q_STATIC_ASSERT(broken::convertPointMember(-16) == s32(0xfffffe00u)); //    down
2844 	Q_STATIC_ASSERT(broken::convertPointMember(-15) == s32(0xfffffe00u)); // __ down __ (should be up)
2845 	Q_STATIC_ASSERT(broken::convertPointMember(-14) == s32(0xffffff00u)); //     up
2846 	Q_STATIC_ASSERT(broken::convertPointMember( -6) == s32(0xffffff00u)); //    down
2847 	Q_STATIC_ASSERT(broken::convertPointMember( -5) == s32(0xffffff00u)); // __ down __ (should be up)
2848 	Q_STATIC_ASSERT(broken::convertPointMember( -4) == s32(0x80000000u)); //   wrong    (should be 0x00000000u)
2849 	Q_STATIC_ASSERT(broken::convertPointMember( -3) == s32(0x80000000u)); //   wrong    (should be 0x00000000u)
2850 	Q_STATIC_ASSERT(broken::convertPointMember( -2) == s32(0x80000000u)); //   wrong    (should be 0x00000000u)
2851 	Q_STATIC_ASSERT(broken::convertPointMember( -1) == s32(0x80000000u)); //   wrong    (should be 0x00000000u)
2852 	Q_STATIC_ASSERT(broken::convertPointMember(  0) == s32(0x00000000u)); //  unchanged
2853 	Q_STATIC_ASSERT(broken::convertPointMember( +1) == s32(0x00000000u)); //    down
2854 	Q_STATIC_ASSERT(broken::convertPointMember( +4) == s32(0x00000000u)); // __ down __
2855 	Q_STATIC_ASSERT(broken::convertPointMember( +5) == s32(0x00000100u)); //     up
2856 	Q_STATIC_ASSERT(broken::convertPointMember(+14) == s32(0x00000100u)); // __ down __
2857 	Q_STATIC_ASSERT(broken::convertPointMember(+15) == s32(0x00000200u)); //     up
2858 #endif
2859 }
2860 
convertPoint(qint32 x,qint32 y)2861 OCADPoint OCAD8FileExport::convertPoint(qint32 x, qint32 y)
2862 {
2863 	return { convertPointMember(x), convertPointMember(-y) };
2864 }
2865 
convertPoint(const MapCoord & coord)2866 OCADPoint OCAD8FileExport::convertPoint(const MapCoord& coord)
2867 {
2868 	return convertPoint(coord.nativeX(), coord.nativeY());
2869 }
2870 
convertSize(qint32 size)2871 s32 OCAD8FileExport::convertSize(qint32 size)
2872 {
2873 	return (s32)((size+5) / 10);
2874 }
2875 
convertColor(const MapColor * color) const2876 s16 OCAD8FileExport::convertColor(const MapColor* color) const
2877 {
2878 	int index = map->findColorIndex(color);
2879 	if (index >= 0)
2880 	{
2881 		return uses_registration_color ? (index + 1) : index;
2882 	}
2883 	return 0;
2884 }
2885 
convertTemplateScale(double mapper_scale)2886 double OCAD8FileExport::convertTemplateScale(double mapper_scale)
2887 {
2888 	return mapper_scale * (1 / 0.01);
2889 }
2890 
addStringTruncationWarning(const QString & text,int truncation_pos)2891 void OCAD8FileExport::addStringTruncationWarning(const QString& text, int truncation_pos)
2892 {
2893 	QString temp = text;
2894 	temp.insert(truncation_pos, QLatin1String("|||"));
2895 	addWarning(tr("String truncated (truncation marked with three '|'): %1").arg(temp));
2896 }
2897 
2898 
2899 }  // namespace OpenOrienteering
2900