1 /*******************************************************************
2 
3 Part of the Fritzing project - http://fritzing.org
4 Copyright (c) 2007-2014 Fachhochschule Potsdam - http://fh-potsdam.de
5 
6 Fritzing is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10 
11 Fritzing is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with Fritzing.  If not, see <http://www.gnu.org/licenses/>.
18 
19 ********************************************************************
20 
21 $Revision: 6904 $:
22 $Author: irascibl@gmail.com $:
23 $Date: 2013-02-26 16:26:03 +0100 (Di, 26. Feb 2013) $
24 
25 ********************************************************************/
26 
27 #include "kicadschematic2svg.h"
28 #include "../utils/textutils.h"
29 #include "../utils/graphicsutils.h"
30 #include "../version/version.h"
31 #include "../debugdialog.h"
32 #include "../viewlayer.h"
33 #include "../fsvgrenderer.h"
34 #include "../utils/misc.h"
35 
36 #include <QFile>
37 #include <QTextStream>
38 #include <QObject>
39 #include <QDomDocument>
40 #include <QDomElement>
41 #include <qmath.h>
42 #include <limits>
43 
44 // TODO:
45 //		pin shape: invert, etc.
46 
KicadSchematic2Svg()47 KicadSchematic2Svg::KicadSchematic2Svg() : Kicad2Svg() {
48 }
49 
listDefs(const QString & filename)50 QStringList KicadSchematic2Svg::listDefs(const QString & filename) {
51 	QStringList defs;
52 
53 	QFile file(filename);
54 	if (!file.open(QFile::ReadOnly)) return defs;
55 
56 	QTextStream textStream(&file);
57 	while (true) {
58 		QString line = textStream.readLine();
59 		if (line.isNull()) break;
60 
61 		if (line.startsWith("DEF")) {
62 			QStringList linedefs = line.split(" ", QString::SkipEmptyParts);
63 			if (linedefs.count() > 1) {
64 				defs.append(linedefs[1]);
65 			}
66 		}
67 	}
68 
69 	return defs;
70 }
71 
convert(const QString & filename,const QString & defName)72 QString KicadSchematic2Svg::convert(const QString & filename, const QString & defName)
73 {
74 	initLimits();
75 
76 	QFile file(filename);
77 	if (!file.open(QFile::ReadOnly)) {
78 		throw QObject::tr("unable to open %1").arg(filename);
79 	}
80 
81 	QTextStream textStream(&file);
82 
83 	QString metadata = makeMetadata(filename, "schematic part", defName);
84 	metadata += endMetadata();
85 
86 	QString reference;
87         int textOffset = 0;
88         bool drawPinNumber = true;
89         bool drawPinName = true;
90 	bool gotDef = false;
91 	while (true) {
92 		QString line = textStream.readLine();
93 		if (line.isNull()) {
94 			break;
95 		}
96 
97 		if (line.startsWith("DEF") && line.contains(defName, Qt::CaseInsensitive)) {
98 			QStringList defs = splitLine(line);
99 			if (defs.count() < 8) {
100 				throw QObject::tr("bad schematic definition %1").arg(filename);
101 			}
102 			reference = defs[2];
103 			textOffset = defs[4].toInt();
104 			drawPinName = defs[6] == "Y";
105 			drawPinNumber = defs[5] == "Y";
106 			gotDef = true;
107 			break;
108 		}
109 	}
110 
111 	if (!gotDef) {
112 		throw QObject::tr("schematic part %1 not found in %2").arg(defName).arg(filename);
113 	}
114 
115 	QString contents = "<g id='schematic'>\n";
116 	bool inFPLIST = false;
117 	while (true) {
118 		QString fline = textStream.readLine();
119 		if (fline.isNull()) {
120 			throw QObject::tr("schematic %1 unexpectedly ends (1) in %2").arg(defName).arg(filename);
121 		}
122 
123 		if (fline.contains("ENDDEF")) {
124 			throw QObject::tr("schematic %1 unexpectedly ends (2) in %2").arg(defName).arg(filename);
125 		}
126 
127 		if (fline.startsWith("DRAW")) {
128 			break;
129 		}
130 
131 		if (fline.startsWith("ALIAS")) continue;
132 
133 		if (fline.startsWith("F")) {
134 			contents += convertField(fline);
135 			continue;
136 		}
137 
138 		if (fline.startsWith("$FPLIST")) {
139 			inFPLIST = true;
140 			break;
141 		}
142 	}
143 
144 	while (inFPLIST) {
145 		QString fline = textStream.readLine();
146 		if (fline.isNull()) {
147 			throw QObject::tr("schematic %1 unexpectedly ends (1) in %2").arg(defName).arg(filename);
148 		}
149 
150 		if (fline.startsWith("$ENDFPLIST")) {
151 			inFPLIST = false;
152 			break;
153 		}
154 
155 		if (fline.contains("ENDDEF")) {
156 			throw QObject::tr("schematic %1 unexpectedly ends (2) in %2").arg(defName).arg(filename);
157 		}
158 	}
159 
160 	int pinIndex = 0;
161 	while (true) {
162 		QString line = textStream.readLine();
163 		if (line.isNull()) {
164 			throw QObject::tr("schematic %1 unexpectedly ends (3) in %2").arg(defName).arg(filename);
165 		}
166 
167 		if (line.startsWith("DRAW")) {
168 			continue;
169 		}
170 
171 		if (line.contains("ENDDEF")) {
172 			break;
173 		}
174 		if (line.contains("ENDDRAW")) {
175 			break;
176 		}
177 
178 		if (line.startsWith("S")) {
179 			contents += convertRect(line);
180 		}
181 		else if (line.startsWith("X")) {
182 			// need to look at them all before formatting (I think)
183 			contents += convertPin(line, textOffset, drawPinName, drawPinNumber, pinIndex++);
184 		}
185 		else if (line.startsWith("C")) {
186 			contents += convertCircle(line);
187 		}
188 		else if (line.startsWith("P")) {
189 			contents += convertPoly(line);
190 		}
191 		else if (line.startsWith("A")) {
192 			contents += convertArc(line);
193 		}
194 		else if (line.startsWith("T")) {
195 			contents += convertText(line);
196 		}
197 		else {
198 			DebugDialog::debug("Unknown line " + line);
199 		}
200 	}
201 
202 	contents += "</g>\n";
203 
204 	QString svg = TextUtils::makeSVGHeader(GraphicsUtils::StandardFritzingDPI, GraphicsUtils::StandardFritzingDPI, m_maxX - m_minX, m_maxY - m_minY)
205 					+ m_title + m_description + metadata + offsetMin(contents) + "</svg>";
206 
207 	return svg;
208 }
209 
convertText(const QString & line)210 QString KicadSchematic2Svg::convertText(const QString & line) {
211 	QStringList fs = splitLine(line);
212 	if (fs.count() < 8) {
213 		DebugDialog::debug("bad text " + line);
214 		return "";
215 	}
216 
217 	return convertField(fs[2], fs[3], fs[4], fs[1], "C", "C", fs[8]);
218 }
219 
convertField(const QString & line)220 QString KicadSchematic2Svg::convertField(const QString & line) {
221 	QStringList fs = splitLine(line);
222 	if (fs.count() < 7) {
223 		DebugDialog::debug("bad field " + line);
224 		return "";
225 	}
226 
227 	if (fs[6] == "I") {
228 		// invisible
229 		return "";
230 	}
231 
232 	while (fs.count() < 9) {
233 		fs.append("");
234 	}
235 
236 	return convertField(fs[2], fs[3], fs[4], fs[5], fs[7], fs[8], fs[1]);
237 }
238 
convertField(const QString & xString,const QString & yString,const QString & fontSizeString,const QString & orientation,const QString & hjustify,const QString & vjustify,const QString & t)239 QString KicadSchematic2Svg::convertField(const QString & xString, const QString & yString, const QString & fontSizeString, const QString &orientation,
240 					 const QString & hjustify, const QString & vjustify, const QString & t)
241 {
242 	QString text = t;
243 	bool notName = false;
244 	if (text.startsWith("~")) {
245 		notName = true;
246 		text.remove(0, 1);
247 	}
248 
249 	int x = xString.toInt();
250 	int y = -yString.toInt();						// KiCad flips y-axis w.r.t. svg
251 	int fontSize = fontSizeString.toInt();
252 
253 	bool rotate = (orientation == "V");
254 	QString rotation;
255 	QMatrix m;
256 	if (rotate) {
257 		m = QMatrix().translate(-x, -y) * QMatrix().rotate(-90) * QMatrix().translate(x, y);
258 		// store x, y, and r so they can be shifted correctly later
259 		rotation = QString("transform='%1' _x='%2' _y='%3' _r='-90'").arg(TextUtils::svgMatrix(m)).arg(x).arg(y);
260 	}
261 
262 	QFont font;
263 	font.setFamily(OCRAFontName);
264 	font.setWeight(QFont::Normal);
265 	font.setPointSizeF(72.0 * fontSize / GraphicsUtils::StandardFritzingDPI);
266 
267 	QString style;
268 	if (vjustify.contains("I")) {
269 		style += "font-style='italic' ";
270 		font.setStyle(QFont::StyleItalic);
271 	}
272 	if (vjustify.endsWith("B")) {
273 		style += "font-weight='bold' ";
274 		font.setWeight(QFont::Bold);
275 	}
276 	QString anchor = "middle";
277 	if (vjustify.startsWith("T")) {
278 		anchor = "end";
279 	}
280 	else if (vjustify.startsWith("B")) {
281 		anchor = "start";
282 	}
283 	if (hjustify.contains("L")) {
284 		anchor = "start";
285 	}
286 	else if (hjustify.contains("R")) {
287 		anchor = "end";
288 	}
289 
290 	QFontMetricsF metrics(font);
291 	QRectF bri = metrics.boundingRect(text);
292 
293 	// convert back to 1000 dpi
294 	QRectF brf(0, 0,
295 			   bri.width() * GraphicsUtils::StandardFritzingDPI / GraphicsUtils::SVGDPI,
296 			   bri.height() * GraphicsUtils::StandardFritzingDPI / GraphicsUtils::SVGDPI);
297 
298 	if (anchor == "start") {
299 		brf.translate(x, y - (brf.height() / 2));
300 	}
301 	else if (anchor == "end") {
302 		brf.translate(x - brf.width(), y - (brf.height() / 2));
303 	}
304 	else if (anchor == "middle") {
305 		brf.translate(x - (brf.width() / 2), y - (brf.height() / 2));
306 	}
307 
308 	if (rotate) {
309 		brf = m.map(QPolygonF(brf)).boundingRect();
310 	}
311 
312 	checkXLimit(brf.left());
313 	checkXLimit(brf.right());
314 	checkYLimit(brf.top());
315 	checkYLimit(brf.bottom());
316 
317 	QString s = QString("<text x='%1' y='%2' font-size='%3' font-family='%8' stroke='none' fill='#000000' text-anchor='%4' %5 %6>%7</text>\n")
318 					.arg(x)
319 					.arg(y + (fontSize / 3))
320 					.arg(fontSize)
321 					.arg(anchor)
322 					.arg(style)
323 					.arg(rotation)
324 					.arg(TextUtils::escapeAnd(unquote(text)))
325                     .arg(OCRAFontName)
326                     ;
327 	if (notName) {
328 		s += QString("<line fill='none' stroke='#000000' x1='%1' y1='%2' x2='%3' y2='%4' stroke-width='2' />\n")
329 			.arg(brf.left())
330 			.arg(brf.top())
331 			.arg(rotate ? brf.left() : brf.right())
332 			.arg(rotate ? brf.bottom() : brf.top());
333 	}
334 	return s;
335 }
336 
convertRect(const QString & line)337 QString KicadSchematic2Svg::convertRect(const QString & line)
338 {
339 	QStringList s = splitLine(line);
340 	if (s.count() < 8) {
341 		DebugDialog::debug(QString("bad rectangle %1").arg(line));
342 		return "";
343 	}
344 
345 	if (s.count() < 9) {
346 		s.append("N");				// assume it's unfilled
347 	}
348 
349 	int x = s[1].toInt();
350 	int y = -s[2].toInt();					// KiCad flips y-axis w.r.t. svg
351 	int x2 = s[3].toInt();
352 	int y2 = -s[4].toInt();					// KiCad flips y-axis w.r.t. svg
353 
354 	checkXLimit(x);
355 	checkXLimit(x2);
356 	checkYLimit(y);
357 	checkYLimit(y2);
358 
359 	QString rect = QString("<rect x='%1' y='%2' width='%3' height='%4' ")
360 			.arg(qMin(x, x2))
361 			.arg(qMin(y, y2))
362 			.arg(qAbs(x2 - x))
363 			.arg(qAbs(y2 - y));
364 
365 	rect += addFill(line, s[8], s[7]);
366 	rect += " />\n";
367 	return rect;
368 }
369 
convertPin(const QString & line,int textOffset,bool drawPinName,bool drawPinNumber,int pinIndex)370 QString KicadSchematic2Svg::convertPin(const QString & line, int textOffset, bool drawPinName, bool drawPinNumber, int pinIndex)
371 {
372 	QStringList l = splitLine(line);
373 	if (l.count() < 12) {
374 		DebugDialog::debug(QString("bad line %1").arg(line));
375 		return "";
376 	}
377 
378 	if (l[6].length() != 1) {
379 		DebugDialog::debug(QString("bad orientation %1").arg(line));
380 		return "";
381 	}
382 
383 	if (l.count() > 12 && l[12] == "N") {
384 		// don't draw this
385 		return "";
386 	}
387 
388 	int unit = l[9].toInt();
389 	if (unit > 1) {
390 		// don't draw this
391 		return "";
392 	}
393 
394 	QChar orientation = l[6].at(0);
395 	QString name = l[1];
396 	if (name == "~") {
397 		name = "";
398 	}
399 	bool pinNumberOK;
400 	int pinNumber = l[2].toInt(&pinNumberOK);
401 	if (!pinNumberOK) {
402 		pinNumber = pinIndex;
403 	}
404 	int nFontSize = l[7].toInt();
405 	int x1 = l[3].toInt();
406 	int y1 = -l[4].toInt();							// KiCad flips y-axis w.r.t. svg
407 	int length = l[5].toInt();
408 	int x2 = x1;
409 	int y2 = y1;
410 	int x3 = x1;
411 	int y3 = y1;
412 	int x4 = x1;
413 	int y4 = y1;
414 	QString justify = "C";
415 	bool rotate = false;
416 	switch (orientation.toLatin1()) {
417 		case 'D':
418 			y2 = y1 + length;
419 			y3 = y1 + (length / 2);
420 			if (textOffset == 0) {
421 				x3 += nFontSize / 2;
422 				x4 -= nFontSize / 2;
423 				y4 = y3;
424 				justify = "C";
425 			}
426 			else {
427 				x3 -= nFontSize / 2;
428 				y4 = y2;
429 				justify = "R";			}
430 			rotate = true;
431 			break;
432 		case 'U':
433 			y2 = y1 - length;
434 			y3 = y1 - (length / 2);
435 			if (textOffset == 0) {
436 				x3 += nFontSize / 2;
437 				x4 -= nFontSize / 2;
438 				y4 = y3;
439 				justify = "C";
440 			}
441 			else {
442 				x3 -= nFontSize / 2;
443 				y4 = y2;
444 				justify = "L";
445 			}
446 			rotate = true;
447 			break;
448 		case 'L':
449 			x2 = x1 - length;
450 			x3 = x1 - (length / 2);
451 			if (textOffset == 0) {
452 				y3 += nFontSize / 2;
453 				y4 -= nFontSize / 2;
454 				x4 = x3;
455 				justify = "C";
456 			}
457 			else {
458 				y3 -= nFontSize / 2;
459 				x4 = x2;
460 				justify = "R";
461 			}
462 			break;
463 		case 'R':
464 			x2 = x1 + length;
465 			x3 = x1 + (length / 2);
466 			if (textOffset == 0) {
467 				y3 += nFontSize / 2;
468 				y4 -= nFontSize / 2;
469 				x4 = x3;
470 				justify = "C";
471 			}
472 			else {
473 				y3 -= nFontSize / 2;
474 				x4 = x2;
475 				justify = "L";
476 			}
477 			break;
478 		default:
479 			DebugDialog::debug(QString("bad orientation %1").arg(line));
480 			break;
481 	}
482 
483 	checkXLimit(x1);
484 	checkXLimit(x2);
485 	checkYLimit(y1);
486 	checkYLimit(y2);
487 
488 	int thickness = 1;
489 
490 	QString pin = QString("<line fill='none' stroke='#000000' x1='%1' y1='%2' x2='%3' y2='%4' stroke-width='%5' id='connector%6pin' connectorname='%7' />\n")
491 			.arg(x1)
492 			.arg(y1)
493 			.arg(x2)
494 			.arg(y2)
495 			.arg(thickness)
496 			.arg(pinNumber)
497 			.arg(TextUtils::escapeAnd(name));
498 
499 	pin += QString("<rect fill='none' x='%1' y='%2' width='0' height='0' stroke-width='0' id='connector%3terminal'  />\n")
500 			.arg(x1)
501 			.arg(y1)
502 			.arg(pinNumber);
503 
504 
505 	if (drawPinNumber) {
506 		pin += convertField(QString::number(x3), QString::number(-y3), l[7], rotate ? "V" : "H", "C", "C", l[2]);
507 	}
508 	if (drawPinName && !name.isEmpty()) {
509 		pin += convertField(QString::number(x4), QString::number(-y4), l[8], rotate ? "V" : "H", justify, "C", name);
510 	}
511 
512 	return pin;
513 }
514 
convertCircle(const QString & line)515 QString KicadSchematic2Svg::convertCircle(const QString & line)
516 {
517 	QStringList s = splitLine(line);
518 	if (s.count() < 8) {
519 		DebugDialog::debug(QString("bad circle %1").arg(line));
520 		return "";
521 	}
522 
523 	int x = s[1].toInt();
524 	int y = -s[2].toInt();					// KiCad flips y-axis w.r.t. svg
525 	int r = s[3].toInt();
526 
527 	checkXLimit(x + r);
528 	checkXLimit(x - r);
529 	checkYLimit(y + r);
530 	checkYLimit(y - r);
531 
532 	QString circle = QString("<circle cx='%1' cy='%2' r='%3' ")
533 			.arg(x)
534 			.arg(y)
535 			.arg(r);
536 
537 	circle += addFill(line, s[7], s[6]);
538 	circle += " />\n";
539 	return circle;
540 }
541 
convertArc(const QString & line)542 QString KicadSchematic2Svg::convertArc(const QString & line)
543 {
544 	QStringList s = splitLine(line);
545 	if (s.count() == 9) {
546 		s.append("N");			// assume unfilled
547 	}
548 
549 	bool calcPoints = false;
550 	if (s.count() == 10) {
551 		s.append("N");
552 		s.append("N");
553 		s.append("N");
554 		s.append("N");
555 		calcPoints = true;
556 	}
557 
558 	if (s.count() < 14) {
559 		DebugDialog::debug("bad arc " + line);
560 		return "";
561 	}
562 
563 
564 	int x = s[1].toInt();
565 	int y = -s[2].toInt();					// KiCad flips y-axis w.r.t. svg
566 	int r = s[3].toInt();
567 	double startAngle = (s[4].toInt() % 3600) / 10.0;
568 	double endAngle = (s[5].toInt() % 3600) / 10.0;
569 
570 	double x1 = s[10].toInt();
571 	double y1 = -s[11].toInt();					// KiCad flips y-axis w.r.t. svg
572 	double x2 = s[12].toInt();
573 	double y2 = -s[13].toInt();					// KiCad flips y-axis w.r.t. svg
574 
575 	if (calcPoints) {
576 		x1 = x + (r * cos(startAngle * M_PI / 180.0));
577 		y1 = y - (r * sin(startAngle * M_PI / 180.0));
578 		x2 = x + (r * cos(endAngle * M_PI / 180.0));
579 		y2 = y - (r * sin(endAngle * M_PI / 180.0));
580 	}
581 
582 	// kicad arcs will always sweep < 180, kicad uses multiple arcs for > 180 sweeps
583 	double diffAngle = endAngle - startAngle;
584 	if (diffAngle > 180) diffAngle -= 360;
585 	else if (diffAngle < -180) diffAngle += 360;
586 
587 	// TODO: use actual bounding box of arc for clipping
588 	checkXLimit(x + r);
589 	checkXLimit(x - r);
590 	checkYLimit(y + r);
591 	checkYLimit(y - r);
592 
593 	QString arc = QString("<path d='M%1,%2a%3,%4 0 %5,%6 %7,%8' ")
594 			.arg(x1)
595 			.arg(y1)
596 			.arg(r)
597 			.arg(r)
598 			.arg(qAbs(diffAngle) >= 180 ? 1 : 0)
599 			.arg(diffAngle > 0 ? 0 : 1)
600 			.arg(x2 - x1)
601 			.arg(y2 - y1);
602 
603 	arc += addFill(line, s[9], s[8]);
604 	arc += " />\n";
605 	return arc;
606 }
607 
convertPoly(const QString & line)608 QString KicadSchematic2Svg::convertPoly(const QString & line)
609 {
610 	QStringList s = splitLine(line);
611 	if (s.count() < 6) {
612 		DebugDialog::debug(QString("bad poly %1").arg(line));
613 		return "";
614 	}
615 
616 	int np = s[1].toInt();
617 	if (np < 2) {
618 		DebugDialog::debug(QString("degenerate poly %1").arg(line));
619 		return "";
620 	}
621 	if (s.count() < (np * 2) + 5) {
622 		DebugDialog::debug(QString("bad poly (2) %1").arg(line));
623 		return "";
624 	}
625 
626 	if (s.count() < (np * 2) + 6) {
627 		s.append("N");				// assume unfilled
628 	}
629 
630 	int ix = 5;
631 	if (np == 2) {
632 		int x1 = s[ix++].toInt();
633 		int y1 = -s[ix++].toInt();						// KiCad flips y-axis w.r.t. svg
634 		int x2 = s[ix++].toInt();
635 		int y2 = -s[ix++].toInt();						// KiCad flips y-axis w.r.t. svg
636 		checkXLimit(x1);
637 		checkYLimit(y1);
638 		checkXLimit(x2);
639 		checkYLimit(y2);
640 		QString line = QString("<line x1='%1' y1='%2' x2='%3' y2='%4' ").arg(x1).arg(y1).arg(x2).arg(y2);
641 		line += addFill(line, s[ix], s[4]);
642 		line += " />\n";
643 		return line;
644 	}
645 
646 	QString poly = "<polyline points='";
647 	for (int i = 0; i < np; i++) {
648 		int x = s[ix++].toInt();
649 		int y = -s[ix++].toInt();						// KiCad flips y-axis w.r.t. svg
650 		checkXLimit(x);
651 		checkYLimit(y);
652 		poly += QString("%1,%2,").arg(x).arg(y);
653 	}
654 	poly.chop(1);
655 	poly += "' ";
656 	poly += addFill(line, s[ix], s[4]);
657 	poly += " />\n";
658 	return poly;
659 }
660 
addFill(const QString & line,const QString & NF,const QString & strokeString)661 QString KicadSchematic2Svg::addFill(const QString & line, const QString & NF, const QString & strokeString) {
662 	int stroke = strokeString.toInt();
663 	if (stroke <= 0) stroke = 1;
664 
665 	if (NF == "F") {
666 		return "fill='#000000' ";
667 	}
668 	else if (NF == "f") {
669 		return "fill='#000000' fill-opacity='0.3' ";
670 	}
671 	else if (NF == "N") {
672 		return QString("fill='none' stroke='#000000' stroke-width='%1' ").arg(stroke);
673 	}
674 
675 
676 	DebugDialog::debug(QString("bad NF param: %1").arg(line));
677 	return "";
678 }
679 
splitLine(const QString & line)680 QStringList KicadSchematic2Svg::splitLine(const QString & line) {
681 	// doesn't handle escaped quotes next to spaces
682 	QStringList strs = line.split(" ", QString::SkipEmptyParts);
683 	for (int i = strs.count() - 1; i > 0; i--) {
684 		QString s = strs[i];
685 		if (s[s.length() - 1] != '"') continue;
686 
687 		if (s[0] == '"' && s.length() > 1) continue;
688 
689 		// space in a quoted string: combine
690 		strs[i - 1] += strs[i];
691 		strs.removeAt(i);
692 	}
693 
694 	return strs;
695 }
696