1
/*******************************************************************
3
Part of the Fritzing project - http://fritzing.org
4
Copyright (c) 2007-2011 Fachhochschule Potsdam - http://fh-potsdam.de
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.
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.
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/>.
19
********************************************************************
22
$Author: cohen@irascible.com $:
23
$Date: 2012-03-28 13:26:38 +0200 (Wed, 28 Mar 2012) $
25
********************************************************************/
27
#include <QMessageBox>
28
#include <QFileDialog>
29
#include <QSvgRenderer>
32
#include "gerbergenerator.h"
33
#include "../debugdialog.h"
34
#include "../fsvgrenderer.h"
35
#include "../sketch/pcbsketchwidget.h"
36
#include "svgfilesplitter.h"
37
#include "groundplanegenerator.h"
38
#include "../utils/graphicsutils.h"
39
#include "../utils/textutils.h"
40
#include "../utils/folderutils.h"
42
static QRegExp AaCc("[aAcCqQtTsS]");
44
const QString GerberGenerator::SilkTopSuffix = "_silkTop.gto";
45
const QString GerberGenerator::SilkBottomSuffix = "_silkBottom.gbo";
46
const QString GerberGenerator::CopperTopSuffix = "_copperTop.gtl";
47
const QString GerberGenerator::CopperBottomSuffix = "_copperBottom.gbl";
48
const QString GerberGenerator::MaskTopSuffix = "_maskTop.gts";
49
const QString GerberGenerator::MaskBottomSuffix = "_maskBottom.gbs";
50
const QString GerberGenerator::DrillSuffix = "_drill.txt";
51
const QString GerberGenerator::OutlineSuffix = "_contour.gm1";
52
const QString GerberGenerator::MagicBoardOutlineID = "boardoutline";
54
const double GerberGenerator::MaskClearanceMils = 3;
56
////////////////////////////////////////////
58
bool pixelsCollide(QImage * image1, QImage * image2, int x1, int y1, int x2, int y2) {
59
for (int y = y1; y < y2; y++) {
60
for (int x = x1; x < x2; x++) {
61
QRgb p1 = image1->pixel(x, y);
62
if (p1 == 0xffffffff) continue;
64
QRgb p2 = image2->pixel(x, y);
65
if (p2 == 0xffffffff) continue;
67
//DebugDialog::debug(QString("p1:%1 p2:%2").arg(p1, 0, 16).arg(p2, 0, 16));
76
////////////////////////////////////////////
78
void GerberGenerator::exportToGerber(const QString & filename, const QString & exportDir, ItemBase * board, PCBSketchWidget * sketchWidget, bool displayMessageBoxes)
81
QList<ItemBase *> boards = sketchWidget->findBoard();
82
if (boards.count() == 0) {
83
DebugDialog::debug("board not found");
86
if (boards.count() > 1) {
87
DebugDialog::debug("multiple boards found");
94
LayerList viewLayerIDs = ViewLayer::copperLayers(ViewLayer::Bottom);
95
int copperInvalidCount = doCopper(board, sketchWidget, viewLayerIDs, "Copper0", CopperBottomSuffix, filename, exportDir, displayMessageBoxes);
97
if (sketchWidget->boardLayers() == 2) {
98
viewLayerIDs = ViewLayer::copperLayers(ViewLayer::Top);
99
copperInvalidCount += doCopper(board, sketchWidget, viewLayerIDs, "Copper1", CopperTopSuffix, filename, exportDir, displayMessageBoxes);
102
LayerList maskLayerIDs = ViewLayer::maskLayers(ViewLayer::Bottom);
103
QString maskBottom, maskTop;
104
int maskInvalidCount = doMask(maskLayerIDs, "Mask0", MaskBottomSuffix, board, sketchWidget, filename, exportDir, displayMessageBoxes, maskBottom);
106
if (sketchWidget->boardLayers() == 2) {
107
maskLayerIDs = ViewLayer::maskLayers(ViewLayer::Top);
108
maskInvalidCount += doMask(maskLayerIDs, "Mask1", MaskTopSuffix, board, sketchWidget, filename, exportDir, displayMessageBoxes, maskTop);
111
LayerList silkLayerIDs = ViewLayer::silkLayers(ViewLayer::Top);
112
int silkInvalidCount = doSilk(silkLayerIDs, "Silk1", SilkTopSuffix, board, sketchWidget, filename, exportDir, displayMessageBoxes, maskTop);
113
silkLayerIDs = ViewLayer::silkLayers(ViewLayer::Bottom);
114
silkInvalidCount += doSilk(silkLayerIDs, "Silk0", SilkBottomSuffix, board, sketchWidget, filename, exportDir, displayMessageBoxes, maskBottom);
116
// now do it for the outline/contour
117
LayerList outlineLayerIDs = ViewLayer::outlineLayers();
120
QString svgOutline = sketchWidget->renderToSVG(FSvgRenderer::printerScale(), outlineLayerIDs, true, imageSize, board, GraphicsUtils::StandardFritzingDPI, false, false, false, empty);
121
if (svgOutline.isEmpty()) {
122
displayMessage(QObject::tr("outline is empty"), displayMessageBoxes);
126
svgOutline = cleanOutline(svgOutline);
127
svgOutline = clipToBoard(svgOutline, board, "board", SVG2gerber::ForOutline, "");
129
QXmlStreamReader streamReader(svgOutline);
130
QSizeF svgSize = FSvgRenderer::parseForWidthAndHeight(streamReader);
132
// create outline gerber from svg
133
SVG2gerber outlineGerber;
134
int outlineInvalidCount = outlineGerber.convert(svgOutline, sketchWidget->boardLayers() == 2, "contour", SVG2gerber::ForOutline, svgSize * GraphicsUtils::StandardFritzingDPI);
136
//DebugDialog::debug(QString("outline output: %1").arg(outlineGerber.getGerber()));
137
saveEnd("contour", exportDir, filename, OutlineSuffix, displayMessageBoxes, true, outlineGerber);
139
doDrill(board, sketchWidget, filename, exportDir, displayMessageBoxes);
141
if (outlineInvalidCount > 0 || silkInvalidCount > 0 || copperInvalidCount > 0 || maskInvalidCount) {
143
if (outlineInvalidCount > 0) s += QObject::tr("the board outline layer, ");
144
if (silkInvalidCount > 0) s += QObject::tr("silkscreen layer(s), ");
145
if (copperInvalidCount > 0) s += QObject::tr("copper layer(s), ");
146
if (maskInvalidCount > 0) s += QObject::tr("mask layer(s), ");
148
displayMessage(QObject::tr("Unable to translate svg curves in %1").arg(s), displayMessageBoxes);
153
int GerberGenerator::doCopper(ItemBase * board, PCBSketchWidget * sketchWidget, LayerList & viewLayerIDs, const QString & copperName, const QString & copperSuffix, const QString & filename, const QString & exportDir, bool displayMessageBoxes)
157
QString svg = sketchWidget->renderToSVG(FSvgRenderer::printerScale(), viewLayerIDs, true, imageSize, board, GraphicsUtils::StandardFritzingDPI, false, false, false, empty);
159
displayMessage(QObject::tr("%1 file export failure (1)").arg(copperName), displayMessageBoxes);
163
QXmlStreamReader streamReader(svg);
164
QSizeF svgSize = FSvgRenderer::parseForWidthAndHeight(streamReader);
166
svg = clipToBoard(svg, board, copperName, SVG2gerber::ForCopper, "");
168
displayMessage(QObject::tr("%1 file export failure (3)").arg(copperName), displayMessageBoxes);
172
return doEnd(svg, sketchWidget->boardLayers(), copperName, SVG2gerber::ForCopper, svgSize * GraphicsUtils::StandardFritzingDPI, exportDir, filename, copperSuffix, displayMessageBoxes, true);
176
int GerberGenerator::doSilk(LayerList silkLayerIDs, const QString & silkName, const QString & gerberSuffix, ItemBase * board, PCBSketchWidget * sketchWidget, const QString & filename, const QString & exportDir, bool displayMessageBoxes, const QString & clipString)
180
QString svgSilk = sketchWidget->renderToSVG(FSvgRenderer::printerScale(), silkLayerIDs, true, imageSize, board, GraphicsUtils::StandardFritzingDPI, false, false, false, empty);
181
if (svgSilk.isEmpty()) {
182
displayMessage(QObject::tr("silk file export failure (1)"), displayMessageBoxes);
187
// don't bother with file
191
//QFile f(silkName + "original.svg");
192
//f.open(QFile::WriteOnly);
193
//QTextStream fs(&f);
197
QXmlStreamReader streamReader(svgSilk);
198
QSizeF svgSize = FSvgRenderer::parseForWidthAndHeight(streamReader);
200
svgSilk = clipToBoard(svgSilk, board, silkName, SVG2gerber::ForSilk, clipString);
201
if (svgSilk.isEmpty()) {
202
displayMessage(QObject::tr("silk export failure"), displayMessageBoxes);
206
//QFile f2(silkName + "clipped.svg");
207
//f2.open(QFile::WriteOnly);
208
//QTextStream fs2(&f2);
212
return doEnd(svgSilk, sketchWidget->boardLayers(), silkName, SVG2gerber::ForSilk, svgSize * GraphicsUtils::StandardFritzingDPI, exportDir, filename, gerberSuffix, displayMessageBoxes, true);
216
int GerberGenerator::doDrill(ItemBase * board, PCBSketchWidget * sketchWidget, const QString & filename, const QString & exportDir, bool displayMessageBoxes)
218
LayerList drillLayerIDs;
219
drillLayerIDs << ViewLayer::drillLayers();
223
QString svgDrill = sketchWidget->renderToSVG(FSvgRenderer::printerScale(), drillLayerIDs, true, imageSize, board, GraphicsUtils::StandardFritzingDPI, false, false, false, empty);
224
if (svgDrill.isEmpty()) {
225
displayMessage(QObject::tr("drill file export failure (1)"), displayMessageBoxes);
230
// don't bother with file
234
QXmlStreamReader streamReader(svgDrill);
235
QSizeF svgSize = FSvgRenderer::parseForWidthAndHeight(streamReader);
237
svgDrill = clipToBoard(svgDrill, board, "Copper0", SVG2gerber::ForDrill, "");
238
if (svgDrill.isEmpty()) {
239
displayMessage(QObject::tr("drill export failure"), displayMessageBoxes);
243
return doEnd(svgDrill, sketchWidget->boardLayers(), "drill", SVG2gerber::ForDrill, svgSize * GraphicsUtils::StandardFritzingDPI, exportDir, filename, DrillSuffix, displayMessageBoxes, true);
246
int GerberGenerator::doMask(LayerList maskLayerIDs, const QString &maskName, const QString & gerberSuffix, ItemBase * board, PCBSketchWidget * sketchWidget, const QString & filename, const QString & exportDir, bool displayMessageBoxes, QString & clipString)
248
// don't want these in the mask laqyer
249
QList<ItemBase *> copperLogoItems;
250
sketchWidget->hideCopperLogoItems(copperLogoItems);
254
QString svgMask = sketchWidget->renderToSVG(FSvgRenderer::printerScale(), maskLayerIDs, true, imageSize, board, GraphicsUtils::StandardFritzingDPI, false, false, false, empty);
255
if (svgMask.isEmpty()) {
256
displayMessage(QObject::tr("mask file export failure (1)"), displayMessageBoxes);
260
sketchWidget->restoreCopperLogoItems(copperLogoItems);
263
// don't bother with file
267
svgMask = TextUtils::expandAndFill(svgMask, "black", MaskClearanceMils * 2);
268
if (svgMask.isEmpty()) {
269
displayMessage(QObject::tr("%1 mask export failure (2)").arg(maskName), displayMessageBoxes);
273
QXmlStreamReader streamReader(svgMask);
274
QSizeF svgSize = FSvgRenderer::parseForWidthAndHeight(streamReader);
276
svgMask = clipToBoard(svgMask, board, maskName, SVG2gerber::ForCopper, "");
277
if (svgMask.isEmpty()) {
278
displayMessage(QObject::tr("mask export failure"), displayMessageBoxes);
282
clipString = svgMask;
284
return doEnd(svgMask, sketchWidget->boardLayers(), maskName, SVG2gerber::ForCopper, svgSize * GraphicsUtils::StandardFritzingDPI, exportDir, filename, gerberSuffix, displayMessageBoxes, true);
287
int GerberGenerator::doEnd(const QString & svg, int boardLayers, const QString & layerName, SVG2gerber::ForWhy forWhy, QSizeF svgSize,
288
const QString & exportDir, const QString & prefix, const QString & suffix, bool displayMessageBoxes, bool chopPrefix)
290
// create mask gerber from svg
292
int invalidCount = gerber.convert(svg, boardLayers == 2, layerName, forWhy, svgSize);
294
saveEnd(layerName, exportDir, prefix, suffix, displayMessageBoxes, chopPrefix, gerber);
299
bool GerberGenerator::saveEnd(const QString & layerName, const QString & exportDir, const QString & prefix, const QString & suffix, bool displayMessageBoxes, bool chopPrefix, SVG2gerber & gerber)
301
QString usePrefix = (chopPrefix) ? QFileInfo(prefix).completeBaseName() : prefix;
303
QString outname = exportDir + "/" + usePrefix + suffix;
305
if (!out.open(QIODevice::WriteOnly | QIODevice::Text)) {
306
displayMessage(QObject::tr("%1 file export failure (2)").arg(layerName), displayMessageBoxes);
310
QTextStream stream(&out);
311
stream << gerber.getGerber();
318
void GerberGenerator::displayMessage(const QString & message, bool displayMessageBoxes) {
319
// don't use QMessageBox if running conversion as a service
320
if (displayMessageBoxes) {
321
QMessageBox::warning(NULL, QObject::tr("Fritzing"), message);
325
DebugDialog::debug(message);
328
QString GerberGenerator::clipToBoard(QString svgString, ItemBase * board, const QString & layerName, SVG2gerber::ForWhy forWhy, const QString & clipString) {
329
QRectF source = board->sceneBoundingRect();
331
return clipToBoard(svgString, source, layerName, forWhy, clipString);
334
QString GerberGenerator::clipToBoard(QString svgString, QRectF & boardRect, const QString & layerName, SVG2gerber::ForWhy forWhy, const QString & clipString) {
335
// document 1 will contain svg that is easy to convert to gerber
336
QDomDocument domDocument1;
340
bool result = domDocument1.setContent(svgString, &errorStr, &errorLine, &errorColumn);
345
QDomElement root = domDocument1.documentElement();
346
if (root.firstChildElement().isNull()) {
350
// document 2 will contain svg that must be rasterized for gerber conversion
351
QDomDocument domDocument2;
352
domDocument2.setContent(svgString, &errorStr, &errorLine, &errorColumn);
354
bool anyConverted = false;
355
if (TextUtils::squashElement(domDocument1, "text", "", QRegExp())) {
359
// gerber can't handle ellipses that are rotated, so cull them all
360
if (TextUtils::squashElement(domDocument1, "ellipse", "", QRegExp())) {
364
if (TextUtils::squashElement(domDocument1, "rect", "rx", QRegExp())) {
368
if (TextUtils::squashElement(domDocument1, "rect", "ry", QRegExp())) {
372
// gerber can't handle paths with curves
373
if (TextUtils::squashElement(domDocument1, "path", "d", AaCc)) {
377
QVector <QDomElement> leaves1;
378
int transformCount1 = 0;
379
QDomElement e1 = domDocument1.documentElement();
380
TextUtils::collectLeaves(e1, transformCount1, leaves1);
382
QVector <QDomElement> leaves2;
383
int transformCount2 = 0;
384
QDomElement e2 = domDocument2.documentElement();
385
TextUtils::collectLeaves(e2, transformCount2, leaves2);
387
double res = GraphicsUtils::StandardFritzingDPI;
388
// convert from pixel dpi to StandardFritzingDPI
389
QRectF sourceRes(boardRect.left() * res / FSvgRenderer::printerScale(), boardRect.top() * res / FSvgRenderer::printerScale(),
390
boardRect.width() * res / FSvgRenderer::printerScale(), boardRect.height() * res / FSvgRenderer::printerScale());
391
int twidth = sourceRes.width();
392
int theight = sourceRes.height();
393
QSize imgSize(twidth, theight);
395
QImage * clipImage = NULL;
396
if (!clipString.isEmpty()) {
397
clipImage = new QImage(imgSize, QImage::Format_Mono);
398
clipImage->fill(0xffffffff);
399
clipImage->setDotsPerMeterX(res * GraphicsUtils::InchesPerMeter);
400
clipImage->setDotsPerMeterY(res * GraphicsUtils::InchesPerMeter);
401
QRectF target(0, 0, twidth, theight);
403
QXmlStreamReader reader(clipString);
404
QSvgRenderer renderer(&reader);
406
painter.begin(clipImage);
407
renderer.render(&painter, target);
411
svgString = TextUtils::removeXMLEntities(domDocument1.toString());
413
QXmlStreamReader reader(svgString);
414
QSvgRenderer renderer(&reader);
415
bool anyClipped = false;
416
for (int i = 0; i < transformCount1; i++) {
417
QString n = QString::number(i);
418
QRectF bounds = renderer.boundsOnElement(n);
419
QMatrix m = renderer.matrixForElement(n);
420
QDomElement element = leaves1.at(i);
421
QRectF mBounds = m.mapRect(bounds);
422
if (mBounds.left() < sourceRes.left() - 0.1|| mBounds.top() < sourceRes.top() - 0.1 || mBounds.right() > sourceRes.right() + 0.1 || mBounds.bottom() > sourceRes.bottom() + 0.1) {
423
// element is outside of bounds--squash it so it will be clipped
424
// we don't care if the board shape is irregular
425
// since anything printed between the shape and the bounding rectangle
426
// will be physically clipped when the board is cut out
427
element.setTagName("g");
428
anyClipped = anyConverted = true;
432
if (forWhy == SVG2gerber::ForOutline) {
433
// make one big polygon. Assume there are no holes in the board
435
if (anyConverted || leaves1.count() > 1) {
437
foreach (QDomElement l, leaves1) {
444
QImage another(imgSize, QImage::Format_Mono);
445
another.fill(0xffffffff);
446
another.setDotsPerMeterX(res * GraphicsUtils::InchesPerMeter);
447
another.setDotsPerMeterY(res * GraphicsUtils::InchesPerMeter);
448
QRectF target(0, 0, twidth, theight);
450
svgString = TextUtils::removeXMLEntities(domDocument1.toString());
451
QXmlStreamReader reader(svgString);
452
QSvgRenderer renderer(&reader);
454
painter.begin(&another);
455
renderer.render(&painter, target);
458
for (int i = 0; i < transformCount1; i++) {
459
QDomElement element = leaves1.at(i);
460
if (element.tagName().compare("g") == 0) {
461
// element is already converted to raster space, we'll clip it later
465
QString n = QString::number(i);
466
QRectF bounds = renderer.boundsOnElement(n);
467
QMatrix m = renderer.matrixForElement(n);
468
QRectF mBounds = m.mapRect(bounds);
471
int x1 = qFloor(qMax(0.0, mBounds.left() - sourceRes.left()));
472
int x2 = qCeil(qMin(sourceRes.width(), mBounds.right() - sourceRes.left()));
473
int y1 = qFloor(qMax(0.0, mBounds.top() - sourceRes.top()));
474
int y2 = qCeil(qMin(sourceRes.height(), mBounds.bottom() - sourceRes.top()));
476
if (pixelsCollide(&another, clipImage, x1, y1, x2, y2)) {
477
element.setTagName("g");
478
anyClipped = anyConverted = true;
484
// svg has been changed by clipping process so get the string again
485
svgString = TextUtils::removeXMLEntities(domDocument1.toString());
489
for (int i = 0; i < transformCount1; i++) {
490
QDomElement element1 = leaves1.at(i);
491
if (element1.tagName().compare("g") != 0) {
492
// document 1 element svg can be directly converted to gerber
493
// so remove it from document 2
494
QDomElement element2 = leaves2.at(i);
495
element2.setTagName("g");
500
// expand the svg to fill the space of the image
501
QDomElement root = domDocument2.documentElement();
502
root.setAttribute("width", QString("%1px").arg(twidth));
503
root.setAttribute("height", QString("%1px").arg(theight));
504
if (boardRect.x() != 0 || boardRect.y() != 0) {
505
QString viewBox = root.attribute("viewBox");
506
QStringList coords = viewBox.split(" ", QString::SkipEmptyParts);
507
coords[0] = QString::number(sourceRes.left());
508
coords[1] = QString::number(sourceRes.top());
509
root.setAttribute("viewBox", coords.join(" "));
512
QString svg = TextUtils::removeXMLEntities(domDocument2.toString());
514
QStringList exceptions;
515
exceptions << "none" << "";
516
QString toColor("#000000");
517
QByteArray svgByteArray;
518
SvgFileSplitter::changeColors(svg, toColor, exceptions, svgByteArray);
520
QImage image(imgSize, QImage::Format_Mono);
521
image.fill(0xffffffff);
522
image.setDotsPerMeterX(res * GraphicsUtils::InchesPerMeter);
523
image.setDotsPerMeterY(res * GraphicsUtils::InchesPerMeter);
524
QRectF target(0, 0, twidth, theight);
526
QSvgRenderer renderer(svgByteArray);
528
painter.begin(&image);
529
renderer.render(&painter, target);
531
image.invertPixels(); // need white pixels on a black background for GroundPlaneGenerator
533
if (clipImage != NULL) {
534
// can this be done with a single blt using composition mode
535
// if not, grab a scanline instead of testing every pixel
536
for (int y = 0; y < theight; y++) {
537
for (int x = 0; x < twidth; x++) {
538
if (clipImage->pixel(x, y) != 0xffffffff) {
539
image.setPixel(x, y, 0);
546
image.save("output.png");
549
GroundPlaneGenerator gpg;
550
gpg.setLayerName(layerName);
551
gpg.setMinRunSize(1, 1);
552
if (forWhy == SVG2gerber::ForOutline) {
553
// int tinyWidth = boardRect.width();
554
// int tinyHeight = boardRect.height();
555
// QRectF tinyTarget(0, 0, tinyWidth, tinyHeight);
556
// QImage tinyImage(tinyWidth, tinyHeight, QImage::Format_Mono);
558
// painter.begin(&tinyImage);
559
// renderer.render(&painter, tinyTarget);
561
// tinyImage.invertPixels(); // need white pixels on a black background for GroundPlaneGenerator
562
gpg.scanOutline(image, image.width(), image.height(), GraphicsUtils::StandardFritzingDPI / res, GraphicsUtils::StandardFritzingDPI, "#000000", false, false, QSizeF(0, 0), 0);
565
gpg.scanImage(image, image.width(), image.height(), GraphicsUtils::StandardFritzingDPI / res, GraphicsUtils::StandardFritzingDPI, "#000000", false, false, QSizeF(0, 0), 0, sourceRes.topLeft());
568
if (gpg.newSVGs().count() > 0) {
570
TextUtils::mergeSvg(doc, svgString, "");
571
foreach (QString gsvg, gpg.newSVGs()) {
572
TextUtils::mergeSvg(doc, gsvg, "");
574
svgString = TextUtils::mergeSvgFinish(doc);
578
if (clipImage) delete clipImage;
583
QString GerberGenerator::cleanOutline(const QString & outlineSvg)
586
doc.setContent(outlineSvg);
587
QList<QDomElement> leaves;
588
QDomElement root = doc.documentElement();
589
TextUtils::collectLeaves(root, leaves);
591
if (leaves.count() == 0) return "";
592
if (leaves.count() == 1) return outlineSvg;
594
if (leaves.count() > 1) {
595
for (int i = 0; i < leaves.count(); i++) {
596
QDomElement leaf = leaves.at(i);
597
if (leaf.attribute("id", "").compare(MagicBoardOutlineID) == 0) {
598
for (int j = 0; j < leaves.count(); j++) {
600
leaves.at(j).parentNode().removeChild(leaves.at(j));
604
return doc.toString();
609
if (leaves.count() == 0) return "";