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 "gedaelement2svg.h"
28 #include "gedaelementparser.h"
29 #include "gedaelementlexer.h"
30 #include "../utils/textutils.h"
31 #include "../version/version.h"
32 #include "../items/wire.h"
33 #include "../debugdialog.h"
34 #include "../fsvgrenderer.h"
35
36 #include <QFile>
37 #include <QFileInfo>
38 #include <QTextStream>
39 #include <QObject>
40 #include <limits>
41 #include <QDomDocument>
42 #include <QDomElement>
43 #include <QDateTime>
44 #include <qmath.h>
45 #include <QTextDocument>
46
GedaElement2Svg()47 GedaElement2Svg::GedaElement2Svg() : X2Svg() {
48 }
49
convert(const QString & filename,bool allowPadsAndPins)50 QString GedaElement2Svg::convert(const QString & filename, bool allowPadsAndPins)
51 {
52 m_nonConnectorNumber = 0;
53 initLimits();
54
55 QFile file(filename);
56 if (!file.open(QFile::ReadOnly)) {
57 throw QObject::tr("unable to open %1").arg(filename);
58 }
59
60 QString text;
61 QTextStream textStream(&file);
62 text = textStream.readAll();
63 file.close();
64
65 GedaElementLexer lexer(text);
66 GedaElementParser parser;
67
68 if (!parser.parse(&lexer)) {
69 throw QObject::tr("unable to parse %1").arg(filename);
70 }
71
72 QFileInfo fileInfo(filename);
73
74 QDateTime now = QDateTime::currentDateTime();
75 QString dt = now.toString("dd/MM/yyyy hh:mm:ss");
76
77 QString title = QString("<title>%1</title>").arg(fileInfo.fileName());
78 QString description = QString("<desc>Geda footprint file '%1' converted by Fritzing</desc>")
79 .arg(TextUtils::stripNonValidXMLCharacters(TextUtils::escapeAnd(fileInfo.fileName())));
80
81
82 QString metadata("<metadata xmlns:fz='http://fritzing.org/gedametadata/1.0/' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>");
83 metadata += "<rdf:RDF>";
84 metadata += "<rdf:Description rdf:about=''>";
85 metadata += m_attribute.arg("geda filename").arg(fileInfo.fileName());
86 metadata += m_attribute.arg("fritzing version").arg(Version::versionString());
87 metadata += m_attribute.arg("conversion date").arg(dt);
88
89 // TODO: other layers
90 QString silkscreen;
91
92 QVector<QVariant> stack = parser.symStack();
93
94 bool hasAuthor = false;
95 QMultiHash<QString, QString> pads;
96 QMultiHash<QString, QString> pins;
97 QStringList pinIDs;
98 QStringList padIDs;
99
100 for (int ix = 0; ix < stack.size(); ) {
101 QVariant var = stack[ix];
102 if (var.type() == QVariant::String) {
103 QString thing = var.toString();
104 int argCount = countArgs(stack, ix);
105 bool mils = stack[ix + argCount + 1].toChar() == ')';
106 if (thing.compare("element", Qt::CaseInsensitive) == 0) {
107 }
108 else if (thing.compare("pad", Qt::CaseInsensitive) == 0) {
109 QString pid;
110 QString s = convertPad(stack, ix, argCount, mils, pid);
111 pads.insert(pid, s);
112 if (!padIDs.contains(pid)) {
113 padIDs.append(pid);
114 }
115 }
116 else if (thing.compare("pin", Qt::CaseInsensitive) == 0) {
117 QString pid;
118 QString s = convertPin(stack, ix, argCount, mils, pid);
119 pins.insert(pid, s);
120 if (!pinIDs.contains(pid)) {
121 pinIDs.append(pid);
122 }
123 }
124 else if (thing.compare("elementline", Qt::CaseInsensitive) == 0) {
125 QString unused;
126 silkscreen += convertPad(stack, ix, argCount, mils, unused);
127 }
128 else if (thing.compare("elementarc", Qt::CaseInsensitive) == 0) {
129 silkscreen += convertArc(stack, ix, argCount, mils);
130 }
131 else if (thing.compare("mark", Qt::CaseInsensitive) == 0) {
132 }
133 else if (thing.compare("attribute", Qt::CaseInsensitive) == 0) {
134 QString aname = TextUtils::stripNonValidXMLCharacters(TextUtils::escapeAnd(unquote(stack[ix + 1].toString())));
135 metadata += m_attribute.arg(aname, TextUtils::stripNonValidXMLCharacters(TextUtils::escapeAnd(unquote(stack[ix + 2].toString()))));
136 if (aname.compare("author", Qt::CaseInsensitive) == 0) {
137 hasAuthor = true;
138 }
139 }
140 ix += argCount + 2;
141 }
142 else if (var.type() == QVariant::Char) {
143 // will arrive here at the end of the element
144 // TODO: shouldn't happen otherwise
145 ix++;
146 }
147 else {
148 throw QObject::tr("parse failure in %1").arg(filename);
149 }
150 }
151
152 if (!allowPadsAndPins && pins.count() > 0 && pads.count() > 0) {
153 throw QObject::tr("Sorry, Fritzing can't yet handle both pins and pads together (in %1)").arg(filename);
154 }
155
156 foreach (QString c, lexer.comments()) {
157 metadata += m_comment.arg(TextUtils::stripNonValidXMLCharacters(TextUtils::escapeAnd(c)));
158 }
159
160 if (!hasAuthor) {
161 metadata += m_attribute.arg("dist-license").arg("GPL");
162 metadata += m_attribute.arg("use-license").arg("unlimited");
163 metadata += m_attribute.arg("author").arg("gEDA project");
164 metadata += m_attribute.arg("license-url").arg("http://www.gnu.org/licenses/gpl.html");
165 }
166
167 metadata += "</rdf:Description>";
168 metadata += "</rdf:RDF>";
169 metadata += "</metadata>";
170
171 QString copper0 = makeCopper(pinIDs, pins, filename);
172 QString copper1 = makeCopper(padIDs, pads, filename);
173
174 if (!copper0.isEmpty()) {
175 copper0 = offsetMin("\n<g id='copper0'><g id='copper1'>" + copper0 + "</g></g>\n");
176 }
177 if (!copper1.isEmpty()) {
178 copper1 = offsetMin("\n<g id='copper1'>" + copper1 + "</g>\n");
179 }
180 if (!silkscreen.isEmpty()) {
181 silkscreen = offsetMin("\n<g id='silkscreen'>" + silkscreen + "</g>\n");
182 }
183
184 QString svg = TextUtils::makeSVGHeader(100000, 100000, m_maxX - m_minX, m_maxY - m_minY)
185 + title + description + metadata + copper0 + copper1 + silkscreen + "</svg>";
186
187 return svg;
188 }
189
countArgs(QVector<QVariant> & stack,int ix)190 int GedaElement2Svg::countArgs(QVector<QVariant> & stack, int ix) {
191 int argCount = 0;
192 for (int i = ix + 1; i < stack.size(); i++) {
193 QVariant var = stack[i];
194 if (var.type() == QVariant::Char) {
195 QChar ch = var.toChar();
196 if (ch == ']' || ch == ')') {
197 break;
198 }
199 }
200
201 argCount++;
202 }
203
204 return argCount;
205 }
206
convertPin(QVector<QVariant> & stack,int ix,int argCount,bool mils,QString & pinID)207 QString GedaElement2Svg::convertPin(QVector<QVariant> & stack, int ix, int argCount, bool mils, QString & pinID)
208 {
209 double drill = 0;
210 QString name;
211 QString number;
212
213 //int flags = stack[ix + argCount].toInt();
214 //bool useNumber = (flags & 1) != 0;
215
216 if (argCount == 9) {
217 drill = stack[ix + 6].toInt();
218 name = stack[ix + 7].toString();
219 number = stack[ix + 8].toString();
220 }
221 else if (argCount == 7) {
222 drill = stack[ix + 4].toInt();
223 name = stack[ix + 5].toString();
224 number = stack[ix + 6].toString();
225 }
226 else if (argCount == 6) {
227 drill = stack[ix + 4].toInt();
228 name = stack[ix + 5].toString();
229 }
230 else if (argCount == 5) {
231 name = stack[ix + 4].toString();
232 }
233 else {
234 throw QObject::tr("bad pin argument count");
235 }
236
237
238 pinID = getPinID(number, name, false);
239
240 int cx = stack[ix + 1].toInt();
241 int cy = stack[ix + 2].toInt();
242 double r = stack[ix + 3].toInt() / 2.0;
243 drill /= 2.0;
244
245 if (mils) {
246 // lo res
247 cx *= 100;
248 cy *= 100;
249 r *= 100;
250 drill *= 100;
251 }
252
253 checkXLimit(cx - r);
254 checkXLimit(cx + r);
255 checkYLimit(cy - r);
256 checkYLimit(cy + r);
257
258 double w = r - drill;
259
260 // TODO: what if multiple pins have the same id--need to clear or increment the other ids. also put the pins on a bus?
261 // TODO: if the pin has a name, post it up to the fz as the connector name
262
263
264 QString circle = QString("<circle fill='none' cx='%1' cy='%2' r='%3' id='%4' connectorname='%5' stroke-width='%6' stroke='%7' />")
265 .arg(cx)
266 .arg(cy)
267 .arg(r - (w / 2))
268 .arg(pinID)
269 .arg(TextUtils::stripNonValidXMLCharacters(TextUtils::escapeAnd(name)))
270 .arg(w)
271 .arg(ViewLayer::Copper0Color);
272 return circle;
273 }
274
convertPad(QVector<QVariant> & stack,int ix,int argCount,bool mils,QString & pinID)275 QString GedaElement2Svg::convertPad(QVector<QVariant> & stack, int ix, int argCount, bool mils, QString & pinID)
276 {
277 QString name;
278 QString number;
279
280 int flags = (argCount > 5) ? stack[ix + argCount].toInt() : 0;
281 bool square = (flags & 0x0100) != 0;
282 int x1 = stack[ix + 1].toInt();
283 int y1 = stack[ix + 2].toInt();
284 int x2 = stack[ix + 3].toInt();
285 int y2 = stack[ix + 4].toInt();
286 int thickness = stack[ix + 5].toInt();
287
288 bool isPad = true;
289 if (argCount == 10) {
290 name = stack[ix + 8].toString();
291 number = stack[ix + 9].toString();
292 QString sflags = stack[ix + argCount].toString();
293 if (sflags.contains("square", Qt::CaseInsensitive)) {
294 square = true;
295 }
296 }
297 else if (argCount == 8) {
298 name = stack[ix + 6].toString();
299 number = stack[ix + 7].toString();
300 }
301 else if (argCount == 7) {
302 name = stack[ix + 6].toString();
303 }
304 else if (argCount == 5) {
305 // this is an elementline
306 isPad = false;
307 }
308 else {
309 throw QObject::tr("bad pad argument count");
310 }
311
312 if (isPad) {
313 pinID = getPinID(number, name, true);
314 }
315
316 if (mils) {
317 // lo res
318 x1 *= 100;
319 y1 *= 100;
320 x2 *= 100;
321 y2 *= 100;
322 thickness *= 100;
323 }
324
325 double halft = thickness / 2.0;
326
327 // don't know which of the coordinates is larger so check them all
328 checkXLimit(x1 - halft);
329 checkXLimit(x2 - halft);
330 checkXLimit(x1 + halft);
331 checkXLimit(x2 + halft);
332 checkYLimit(y1 - halft);
333 checkYLimit(y2 - halft);
334 checkYLimit(y1 + halft);
335 checkYLimit(y2 + halft);
336
337 QString line = QString("<line fill='none' x1='%1' y1='%2' x2='%3' y2='%4' stroke-width='%5' ")
338 .arg(x1)
339 .arg(y1)
340 .arg(x2)
341 .arg(y2)
342 .arg(thickness);
343 if (!isPad) {
344 // elementline
345 line += "stroke='white' ";
346 }
347 else {
348 line += QString("stroke-linecap='%1' stroke-linejoin='%2' id='%3' connectorname='%4' stroke='%5' ")
349 .arg(square ? "square" : "round")
350 .arg(square ? "miter" : "round")
351 .arg(pinID)
352 .arg(TextUtils::stripNonValidXMLCharacters(TextUtils::escapeAnd(name)))
353 .arg(ViewLayer::Copper1Color);
354 }
355
356 line += "/>";
357 return line;
358 }
359
convertArc(QVector<QVariant> & stack,int ix,int argCount,bool mils)360 QString GedaElement2Svg::convertArc(QVector<QVariant> & stack, int ix, int argCount, bool mils)
361 {
362 Q_UNUSED(argCount);
363
364 int x = stack[ix + 1].toInt();
365 int y = stack[ix + 2].toInt();
366 double w = stack[ix + 3].toInt();
367 double h = stack[ix + 4].toInt();
368
369 // In PCB, an angle of zero points left (negative X direction), and 90 degrees points down (positive Y direction)
370 int startAngle = (stack[ix + 5].toInt()) + 180;
371 // Positive angles sweep counterclockwise
372 int deltaAngle = stack[ix + 6].toInt();
373
374 int thickness = stack[ix + 7].toInt();
375
376 if (mils) {
377 // lo res
378 x *= 100;
379 y *= 100;
380 w *= 100;
381 h *= 100;
382 thickness *= 100;
383 }
384
385 double halft = thickness / 2.0;
386 checkXLimit(x - w - halft);
387 checkXLimit(x + w + halft);
388 checkYLimit(y - h - halft);
389 checkYLimit(y + h + halft);
390
391 if (deltaAngle == 360) {
392 if (w == h) {
393 QString circle = QString("<circle fill='none' cx='%1' cy='%2' stroke='white' r='%3' stroke-width='%4' />")
394 .arg(x)
395 .arg(y)
396 .arg(w)
397 .arg(thickness);
398
399 return circle;
400 }
401
402 QString ellipse = QString("<ellipse fill='none' cx='%1' cy='%2' stroke='white' rx='%3' ry='%4' stroke-width='%5' />")
403 .arg(x)
404 .arg(y)
405 .arg(w)
406 .arg(h)
407 .arg(thickness);
408
409 return ellipse;
410 }
411
412 int quad = 0;
413 int startAngleQ1 = reflectQuad(startAngle, quad);
414 double q = atan(w * tan(2 * M_PI * startAngleQ1 / 360.0) / h);
415 double px = w * cos(q);
416 double py = -h * sin(q);
417 fixQuad(quad, px, py);
418 int endAngleQ1 = reflectQuad(startAngle + deltaAngle, quad);
419 q = atan(w * tan(2 * M_PI * endAngleQ1 / 360.0) / h);
420 double qx = w * cos(q);
421 double qy = -h * sin(q);
422 fixQuad(quad, qx, qy);
423
424 QString arc = QString("<path fill='none' stroke-width='%1' stroke='white' d='M%2,%3a%4,%5 0 %6,%7 %8,%9' />")
425 .arg(thickness)
426 .arg(px + x)
427 .arg(py + y)
428 .arg(w)
429 .arg(h)
430 .arg(qAbs(deltaAngle) >= 180 ? 1 : 0)
431 .arg(deltaAngle > 0 ? 0 : 1)
432 .arg(qx - px)
433 .arg(qy - py);
434
435 return arc;
436 }
437
fixQuad(int quad,double & px,double & py)438 void GedaElement2Svg::fixQuad(int quad, double & px, double & py) {
439 switch (quad) {
440 case 0:
441 break;
442 case 1:
443 px = -px;
444 break;
445 case 2:
446 px = -px;
447 py = -py;
448 break;
449 case 3:
450 py = -py;
451 break;
452 }
453 }
454
reflectQuad(int angle,int & quad)455 int GedaElement2Svg::reflectQuad(int angle, int & quad) {
456 angle = angle %360;
457 if (angle < 0) angle += 360;
458 quad = angle / 90;
459 switch (quad) {
460 case 0:
461 return angle;
462 case 1:
463 return 180 - angle;
464 case 2:
465 return angle - 180;
466 case 3:
467 return 360 - angle;
468 }
469
470 // never gets here, but keeps compiler happy
471 return angle;
472 }
473
getPinID(QString & number,QString & name,bool isPad)474 QString GedaElement2Svg::getPinID(QString & number, QString & name, bool isPad) {
475
476 if (!number.isEmpty()) {
477 number = unquote(number);
478 }
479 if (!name.isEmpty()) {
480 name = unquote(name);
481 }
482
483 QString suffix = isPad ? "pad" : "pin";
484
485 if (!number.isEmpty()) {
486 bool ok;
487 int n = number.toInt(&ok);
488 return ok ? QString("connector%1%2").arg(n - 1).arg(suffix)
489 : QString("connector%1%2").arg(number).arg(suffix);
490 }
491
492 if (!name.isEmpty()) {
493 if (number.isEmpty()) {
494 bool ok;
495 int n = name.toInt(&ok);
496 if (ok) {
497 return QString("connector%1%2").arg(n - 1).arg(suffix);
498 }
499 }
500 }
501
502 return QString("%1%2").arg(FSvgRenderer::NonConnectorName).arg(m_nonConnectorNumber++);
503 }
504
505
makeCopper(QStringList ids,QHash<QString,QString> & strings,const QString & filename)506 QString GedaElement2Svg::makeCopper(QStringList ids, QHash<QString, QString> & strings, const QString & filename) {
507 QString copper;
508 foreach (QString id, ids) {
509 QStringList values = strings.values(id);
510 if (id.isEmpty()) {
511 DebugDialog::debug(QString("geda empty id %1").arg(filename));
512 foreach(QString string, values) {
513 copper.append(string);
514 }
515 continue;
516 }
517
518 if (values.count() == 1) {
519 copper.append(values.at(0));
520 continue;
521 }
522
523 if (values.count() == 0) {
524 // shouldn't happen
525 continue;
526 }
527
528 DebugDialog::debug(QString("geda multiple id %1").arg(filename));
529
530
531 QString xml = "<g>";
532 foreach (QString string, values) {
533 xml.append(string);
534 }
535 xml.append("</g>");
536 QString errorStr;
537 int errorLine;
538 int errorColumn;
539 QDomDocument doc;
540 if (!doc.setContent(xml, &errorStr, &errorLine, &errorColumn)) {
541 throw QObject::tr("Unable to parse copper: %1 %2 %3").arg(errorStr).arg(errorLine).arg(errorColumn);
542 }
543 QDomElement root = doc.documentElement();
544 QDomElement child = root.firstChildElement();
545 while (!child.isNull()) {
546 QString id = child.attribute("id");
547 root.setAttribute("id", id);
548 child.removeAttribute("id");
549 QString name = child.attribute("connectorname");
550 child.removeAttribute("connectorname");
551 if (!name.isEmpty()) {
552 root.setAttribute("connectorname", name);
553 }
554 child = child.nextSiblingElement();
555 }
556
557 copper += doc.toString();
558 }
559
560 return copper;
561 }
562