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