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