├── .gitignore ├── README.md ├── dtd ├── eagle-6.6.dtd ├── eagle-7.0.dtd ├── eagle-7.1.dtd ├── eagle-7.2.dtd ├── eagle-7.3.dtd ├── eagle-7.4.dtd ├── eagle-7.5.dtd ├── eagle-7.6.dtd └── eagle-7.7.dtd ├── merge.py └── test ├── run.sh ├── test_in.brd └── test_out.brd /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Overview 3 | -------- 4 | 5 | This command-line tool merges several board files produced by CadSoft Eagle CAD 6 | software into one, effectively reimplementing the panelize.ulp script included 7 | in the program itself, but without the board size limit in the light and 8 | freeware editions of the program. 9 | 10 | The tool helps to perform panelization of one or several designs into a board 11 | larger than the board size limit. The output is a regular Eagle board file 12 | which can be easily inspected. Moreover, it's possible to perform various 13 | additional modifications, as Eagle only restricts component movement outside 14 | the board size limits. This is especially useful in cases when the production 15 | facility requires all panelized boards to be connected somehow, which is 16 | the case with most low-cost prototype PCB producers. 17 | 18 | Operation 19 | --------- 20 | 21 | The tool copies the board data optionally rotating the board and adding a 22 | position offset to the coordinates. The included component data is correctly 23 | merged. 24 | 25 | If several input files use the same component from the same library, the 26 | duplicates are removed. The user must ensure that the input files use up-to 27 | date libraries. If there is a mismatch between the definitions of the same 28 | component used in different files, the program aborts. 29 | 30 | The tool may change signal or element names to ensure that resulting output file 31 | does not have signals or elements with duplicate names. This is required by 32 | Eagle. Whenever element name is changed, the tool ensures that the displayed 33 | label stays the same by introducing a custom attribute which is displayed 34 | instead of the name attribute. 35 | 36 | All input files should use the same design rules. If there is a mismatch, the 37 | program aborts. 38 | 39 | This tool does not support board files produced by Eagle versions earlier than 40 | 6.0. Most features of the newer board files are supported. The program aborts 41 | whenever unsupported feature is encountered. 42 | 43 | Usage 44 | ----- 45 | 46 | merge.py output-file [in-file [--offx offset-x] [--offy offset-y] [--rotation rotation]]... 47 | 48 | - `output-file`: path to the output .brd file 49 | - `in-file`: path to an input .brd file 50 | - `offset-x`, `offset-y`: the position offset to apply to the particular input file. 51 | The suffix determines the units. The following suffixes are supported: 52 | - mm: millimeters 53 | - `rotation`: The counter-clockwise rotation in degrees to apply to the 54 | particular input file. The following values are supported: `0`, `90`, `180` 55 | and `270`. 56 | 57 | Requirements 58 | ------------ 59 | 60 | python3 and lxml are required. 61 | 62 | License 63 | ------- 64 | 65 | The tool is licensed under General Public License. 66 | 67 | Copyright (C) 2016 Povilas Kanapickas 68 | 69 | This program is free software: you can redistribute it and/or modify 70 | it under the terms of the GNU General Public License as published by 71 | the Free Software Foundation, either version 3 of the License, or 72 | (at your option) any later version. 73 | This program is distributed in the hope that it will be useful, 74 | but WITHOUT ANY WARRANTY; without even the implied warranty of 75 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 76 | GNU General Public License for more details. 77 | You should have received a copy of the GNU General Public License 78 | along with this program. If not, see http://www.gnu.org/licenses/. 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /dtd/eagle-6.6.dtd: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | 64 | 65 | 66 | 67 | 68 | 69 | 72 | 73 | 74 | 75 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 90 | 91 | 92 | 95 | 96 | 97 | 102 | 103 | 104 | 108 | 109 | 110 | 113 | 114 | 115 | 119 | 120 | 121 | 122 | 123 | 124 | 129 | 130 | 131 | 132 | 133 | 137 | 138 | 139 | 145 | 146 | 147 | 148 | 156 | 157 | 158 | 170 | 171 | 172 | 173 | 174 | 193 | 194 | 195 | 206 | 207 | 208 | 215 | 216 | 217 | 225 | 226 | 227 | 240 | 241 | 242 | 247 | 248 | 249 | 261 | 262 | 263 | 276 | 277 | 278 | 289 | 290 | 291 | 300 | 301 | 302 | 303 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 324 | 325 | 326 | 327 | 338 | 339 | 340 | 348 | 349 | 350 | 358 | 359 | 360 | 361 | 371 | 372 | 373 | 374 | 375 | 379 | 380 | 381 | 387 | 388 | 389 | 392 | 393 | 394 | 407 | 408 | 409 | 410 | 411 | 416 | 417 | 418 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 458 | 459 | 460 | 463 | 464 | 465 | 476 | 477 | 478 | 486 | 487 | 488 | 494 | 495 | 496 | 500 | 501 | 502 | 505 | 506 | 507 | 511 | 512 | 513 | 518 | 519 | 520 | 523 | -------------------------------------------------------------------------------- /dtd/eagle-7.0.dtd: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 80 | 81 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 99 | 100 | 101 | 104 | 105 | 106 | 111 | 112 | 113 | 117 | 118 | 119 | 122 | 123 | 124 | 128 | 129 | 130 | 131 | 132 | 133 | 138 | 139 | 140 | 141 | 142 | 152 | 153 | 154 | 155 | 159 | 160 | 161 | 167 | 168 | 169 | 170 | 178 | 179 | 180 | 192 | 193 | 194 | 195 | 196 | 215 | 216 | 217 | 228 | 229 | 230 | 237 | 238 | 239 | 247 | 248 | 249 | 262 | 263 | 264 | 269 | 270 | 271 | 283 | 284 | 285 | 298 | 299 | 300 | 301 | 313 | 314 | 315 | 324 | 325 | 326 | 327 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 348 | 349 | 350 | 351 | 362 | 363 | 364 | 372 | 373 | 374 | 380 | 381 | 382 | 390 | 391 | 392 | 393 | 403 | 404 | 405 | 406 | 407 | 411 | 412 | 413 | 419 | 420 | 421 | 424 | 425 | 426 | 439 | 440 | 441 | 442 | 443 | 448 | 449 | 450 | 456 | 457 | 458 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 499 | 500 | 501 | 504 | 505 | 506 | 517 | 518 | 519 | 527 | 528 | 529 | 535 | 536 | 537 | 541 | 542 | 543 | 546 | 547 | 548 | 552 | 553 | 554 | 559 | 560 | 561 | 564 | -------------------------------------------------------------------------------- /dtd/eagle-7.1.dtd: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 80 | 81 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 99 | 100 | 101 | 104 | 105 | 106 | 111 | 112 | 113 | 117 | 118 | 119 | 122 | 123 | 124 | 128 | 129 | 130 | 131 | 132 | 133 | 138 | 139 | 140 | 141 | 142 | 152 | 153 | 154 | 155 | 159 | 160 | 161 | 167 | 168 | 169 | 170 | 178 | 179 | 180 | 192 | 193 | 194 | 195 | 196 | 215 | 216 | 217 | 228 | 229 | 230 | 237 | 238 | 239 | 247 | 248 | 249 | 262 | 263 | 264 | 269 | 270 | 271 | 283 | 284 | 285 | 298 | 299 | 300 | 301 | 313 | 314 | 315 | 324 | 325 | 326 | 327 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 348 | 349 | 350 | 351 | 362 | 363 | 364 | 372 | 373 | 374 | 380 | 381 | 382 | 390 | 391 | 392 | 393 | 403 | 404 | 405 | 406 | 407 | 411 | 412 | 413 | 419 | 420 | 421 | 424 | 425 | 426 | 439 | 440 | 441 | 442 | 443 | 448 | 449 | 450 | 456 | 457 | 458 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 499 | 500 | 501 | 504 | 505 | 506 | 517 | 518 | 519 | 527 | 528 | 529 | 535 | 536 | 537 | 541 | 542 | 543 | 546 | 547 | 548 | 552 | 553 | 554 | 559 | 560 | 561 | 564 | -------------------------------------------------------------------------------- /dtd/eagle-7.2.dtd: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 80 | 81 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 99 | 100 | 101 | 104 | 105 | 106 | 111 | 112 | 113 | 117 | 118 | 119 | 122 | 123 | 124 | 128 | 129 | 130 | 131 | 132 | 133 | 138 | 139 | 140 | 141 | 142 | 152 | 153 | 154 | 155 | 159 | 160 | 161 | 167 | 168 | 169 | 170 | 178 | 179 | 180 | 192 | 193 | 194 | 195 | 196 | 215 | 216 | 217 | 228 | 229 | 230 | 237 | 238 | 239 | 247 | 248 | 249 | 262 | 263 | 264 | 269 | 270 | 271 | 283 | 284 | 285 | 298 | 299 | 300 | 301 | 313 | 314 | 315 | 324 | 325 | 326 | 327 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 348 | 349 | 350 | 351 | 362 | 363 | 364 | 372 | 373 | 374 | 380 | 381 | 382 | 390 | 391 | 392 | 393 | 403 | 404 | 405 | 406 | 407 | 411 | 412 | 413 | 419 | 420 | 421 | 424 | 425 | 426 | 439 | 440 | 441 | 442 | 443 | 448 | 449 | 450 | 456 | 457 | 458 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 499 | 500 | 501 | 504 | 505 | 506 | 517 | 518 | 519 | 527 | 528 | 529 | 535 | 536 | 537 | 541 | 542 | 543 | 546 | 547 | 548 | 552 | 553 | 554 | 559 | 560 | 561 | 564 | -------------------------------------------------------------------------------- /dtd/eagle-7.3.dtd: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 80 | 81 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 99 | 100 | 101 | 104 | 105 | 106 | 111 | 112 | 113 | 117 | 118 | 119 | 122 | 123 | 124 | 128 | 129 | 130 | 131 | 132 | 133 | 138 | 139 | 140 | 141 | 142 | 152 | 153 | 154 | 155 | 159 | 160 | 161 | 167 | 168 | 169 | 170 | 178 | 179 | 180 | 192 | 193 | 194 | 195 | 196 | 215 | 216 | 217 | 228 | 229 | 230 | 237 | 238 | 239 | 247 | 248 | 249 | 262 | 263 | 264 | 269 | 270 | 271 | 283 | 284 | 285 | 298 | 299 | 300 | 301 | 313 | 314 | 315 | 324 | 325 | 326 | 327 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 348 | 349 | 350 | 351 | 362 | 363 | 364 | 372 | 373 | 374 | 380 | 381 | 382 | 390 | 391 | 392 | 393 | 403 | 404 | 405 | 406 | 407 | 411 | 412 | 413 | 419 | 420 | 421 | 424 | 425 | 426 | 439 | 440 | 441 | 442 | 443 | 448 | 449 | 450 | 456 | 457 | 458 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 499 | 500 | 501 | 504 | 505 | 506 | 517 | 518 | 519 | 527 | 528 | 529 | 535 | 536 | 537 | 541 | 542 | 543 | 546 | 547 | 548 | 552 | 553 | 554 | 559 | 560 | 561 | 564 | -------------------------------------------------------------------------------- /dtd/eagle-7.4.dtd: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 80 | 81 | 82 | 88 | 89 | 90 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 102 | 103 | 104 | 107 | 108 | 109 | 114 | 115 | 116 | 120 | 121 | 122 | 125 | 126 | 127 | 131 | 132 | 133 | 134 | 135 | 136 | 141 | 142 | 143 | 144 | 145 | 155 | 156 | 157 | 158 | 162 | 163 | 164 | 170 | 171 | 172 | 173 | 181 | 182 | 183 | 195 | 196 | 197 | 198 | 199 | 218 | 219 | 220 | 231 | 232 | 233 | 240 | 241 | 242 | 250 | 251 | 252 | 265 | 266 | 267 | 272 | 273 | 274 | 286 | 287 | 288 | 301 | 302 | 303 | 304 | 316 | 317 | 318 | 327 | 328 | 329 | 330 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 351 | 352 | 353 | 354 | 365 | 366 | 367 | 375 | 376 | 377 | 383 | 384 | 385 | 393 | 394 | 395 | 396 | 406 | 407 | 408 | 409 | 410 | 414 | 415 | 416 | 422 | 423 | 424 | 427 | 428 | 429 | 442 | 443 | 444 | 445 | 446 | 451 | 452 | 453 | 459 | 460 | 461 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 502 | 503 | 504 | 507 | 508 | 509 | 520 | 521 | 522 | 530 | 531 | 532 | 538 | 539 | 540 | 544 | 545 | 546 | 549 | 550 | 551 | 555 | 556 | 557 | 562 | 563 | 564 | 567 | -------------------------------------------------------------------------------- /dtd/eagle-7.5.dtd: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 80 | 81 | 82 | 88 | 89 | 90 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 102 | 103 | 104 | 107 | 108 | 109 | 114 | 115 | 116 | 120 | 121 | 122 | 125 | 126 | 127 | 131 | 132 | 133 | 134 | 135 | 136 | 141 | 142 | 143 | 144 | 145 | 155 | 156 | 157 | 158 | 162 | 163 | 164 | 170 | 171 | 172 | 173 | 181 | 182 | 183 | 195 | 196 | 197 | 198 | 199 | 218 | 219 | 220 | 231 | 232 | 233 | 240 | 241 | 242 | 250 | 251 | 252 | 265 | 266 | 267 | 272 | 273 | 274 | 286 | 287 | 288 | 301 | 302 | 303 | 304 | 316 | 317 | 318 | 327 | 328 | 329 | 330 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 351 | 352 | 353 | 354 | 365 | 366 | 367 | 375 | 376 | 377 | 383 | 384 | 385 | 393 | 394 | 395 | 396 | 406 | 407 | 408 | 409 | 410 | 414 | 415 | 416 | 422 | 423 | 424 | 427 | 428 | 429 | 442 | 443 | 444 | 445 | 446 | 451 | 452 | 453 | 459 | 460 | 461 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 502 | 503 | 504 | 507 | 508 | 509 | 520 | 521 | 522 | 530 | 531 | 532 | 538 | 539 | 540 | 544 | 545 | 546 | 549 | 550 | 551 | 555 | 556 | 557 | 562 | 563 | 564 | 567 | -------------------------------------------------------------------------------- /dtd/eagle-7.6.dtd: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 80 | 81 | 82 | 88 | 89 | 90 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 102 | 103 | 104 | 107 | 108 | 109 | 114 | 115 | 116 | 120 | 121 | 122 | 125 | 126 | 127 | 131 | 132 | 133 | 134 | 135 | 136 | 141 | 142 | 143 | 144 | 145 | 155 | 156 | 157 | 158 | 162 | 163 | 164 | 170 | 171 | 172 | 173 | 181 | 182 | 183 | 195 | 196 | 197 | 198 | 199 | 218 | 219 | 220 | 231 | 232 | 233 | 240 | 241 | 242 | 250 | 251 | 252 | 265 | 266 | 267 | 272 | 273 | 274 | 286 | 287 | 288 | 301 | 302 | 303 | 304 | 316 | 317 | 318 | 327 | 328 | 329 | 330 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 351 | 352 | 353 | 354 | 365 | 366 | 367 | 375 | 376 | 377 | 383 | 384 | 385 | 393 | 394 | 395 | 396 | 406 | 407 | 408 | 409 | 410 | 414 | 415 | 416 | 422 | 423 | 424 | 427 | 428 | 429 | 442 | 443 | 444 | 445 | 446 | 451 | 452 | 453 | 459 | 460 | 461 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 502 | 503 | 504 | 507 | 508 | 509 | 520 | 521 | 522 | 530 | 531 | 532 | 538 | 539 | 540 | 544 | 545 | 546 | 549 | 550 | 551 | 555 | 556 | 557 | 562 | 563 | 564 | 567 | -------------------------------------------------------------------------------- /dtd/eagle-7.7.dtd: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 80 | 81 | 82 | 88 | 89 | 90 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 102 | 103 | 104 | 107 | 108 | 109 | 114 | 115 | 116 | 120 | 121 | 122 | 125 | 126 | 127 | 131 | 132 | 133 | 134 | 135 | 136 | 141 | 142 | 143 | 144 | 145 | 155 | 156 | 157 | 158 | 162 | 163 | 164 | 170 | 171 | 172 | 173 | 181 | 182 | 183 | 195 | 196 | 197 | 198 | 199 | 218 | 219 | 220 | 231 | 232 | 233 | 240 | 241 | 242 | 250 | 251 | 252 | 265 | 266 | 267 | 272 | 273 | 274 | 286 | 287 | 288 | 301 | 302 | 303 | 304 | 316 | 317 | 318 | 327 | 328 | 329 | 330 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 351 | 352 | 353 | 354 | 365 | 366 | 367 | 375 | 376 | 377 | 383 | 384 | 385 | 393 | 394 | 395 | 396 | 406 | 407 | 408 | 409 | 410 | 414 | 415 | 416 | 422 | 423 | 424 | 427 | 428 | 429 | 442 | 443 | 444 | 445 | 446 | 451 | 452 | 453 | 459 | 460 | 461 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 502 | 503 | 504 | 507 | 508 | 509 | 520 | 521 | 522 | 530 | 531 | 532 | 538 | 539 | 540 | 544 | 545 | 546 | 549 | 550 | 551 | 555 | 556 | 557 | 562 | 563 | 564 | 567 | -------------------------------------------------------------------------------- /merge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Copyright (C) 2016 Povilas Kanapickas 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see http://www.gnu.org/licenses/. 17 | ''' 18 | 19 | ''' We operate on the Eagle XML files directly instead of parsing them into 20 | some kind of intermediate structure. We only care about very limited subset 21 | of information, thus complete format support is unnecessary. 22 | ''' 23 | 24 | from copy import deepcopy 25 | import os 26 | import sys 27 | import lxml.etree as etree 28 | from functools import cmp_to_key 29 | import re 30 | 31 | class InputFile: 32 | 33 | def __init__(self): 34 | self.path = None 35 | self.offsetx = 0 36 | self.offsety = 0 37 | self.rotation = 0 38 | 39 | def print_usage_and_exit(): 40 | print('''Usage: 41 | merge.py output-file [in-file [--offx offset-x] [--offy offset-y] [--rotation rotation]]... 42 | ''') 43 | sys.exit(1) 44 | 45 | def print_file_error_and_exit(infile, el, err = "Unexpected element"): 46 | print("For file {0}".format(infile.path)) 47 | print("Error : " + el.getroottree().getpath(el) + " : " + err) 48 | sys.exit(2) 49 | 50 | def print_file_warning(infile, warn): 51 | print("For file {0}".format(infile.path)) 52 | print("Warning : " + warn) 53 | 54 | def parse_offset(val): 55 | if val.endswith('mm'): 56 | try: 57 | return float(val[:-2]) 58 | except: 59 | pass 60 | 61 | print("Can't parse {0} as an offset value. Were units forgotten?".format(val)) 62 | print_usage_and_exit() 63 | 64 | def parse_rotation(val): 65 | if val in ['0', '90', '180', '270']: 66 | return int(val) 67 | print("Can't parse {0} as a rotation value. Supported rotations are 0, 90, 180, 270.".format(val)) 68 | print_usage_and_exit() 69 | 70 | def fetch_arg(num): 71 | if num >= len(sys.argv): 72 | print('Too few arguments specified. Expected at least one more') 73 | print_usage_and_exit() 74 | return sys.argv[num] 75 | 76 | def parse_args(): 77 | 78 | outfile = fetch_arg(1) 79 | infiles = [] 80 | infile = None 81 | 82 | i = 2 83 | while i < len(sys.argv): 84 | arg = fetch_arg(i) 85 | if arg.startswith('-'): 86 | # Apply one option to current input file 87 | if arg == '--offx': 88 | infile.offsetx = parse_offset(fetch_arg(i+1)) 89 | elif arg == '--offy': 90 | infile.offsety = parse_offset(fetch_arg(i+1)) 91 | elif arg == '--rotation': 92 | infile.rotation = parse_rotation(fetch_arg(i+1)) 93 | else: 94 | print("Unsupported option {0}".format(arg)) 95 | print_usage_and_exit() 96 | i = i + 2 97 | else: 98 | # Start with new input file 99 | if infile != None: 100 | infiles.append(infile) 101 | infile = InputFile() 102 | infile.path = arg 103 | i = i + 1 104 | 105 | if infile != None: 106 | infiles.append(infile) 107 | 108 | return (outfile, infiles) 109 | 110 | ''' Retrieves a child of lxml element el which matches the given criteria: 111 | * has matching tag 112 | * has at least the given attributes with matching values. If None is 113 | specified, any value is accepted 114 | If child is not found, None is returned 115 | ''' 116 | def find_child(el, tag, attrs = {}): 117 | for child in el: 118 | if child.tag == tag: 119 | valid = True 120 | for key in attrs: 121 | attr = child.get(key) 122 | if attr != attrs[key] and attrs[key] != None: 123 | valid = False 124 | if valid: 125 | return child 126 | return None 127 | 128 | ''' Same as find_child, except that if the child is not fould, a new one with 129 | the given tag is created. 130 | ''' 131 | def find_or_create_child(el, tag, attrs = {}): 132 | child = find_child(el, tag, attrs) 133 | if child != None: 134 | return child 135 | return etree.SubElement(el, tag) 136 | 137 | ''' Compares two Xml trees 138 | ''' 139 | def xml_tree_compare(a, b): 140 | # compare root node 141 | if a.tag < b.tag: 142 | return -1 143 | elif a.tag > b.tag: 144 | return 1 145 | elif a.tail < b.tail: 146 | return -1 147 | elif a.tail > b.tail: 148 | return 1 149 | 150 | # compare attributes 151 | aitems = a.attrib.items() 152 | aitems.sort() 153 | bitems = b.attrib.items() 154 | bitems.sort() 155 | if aitems < bitems: 156 | return -1 157 | elif aitems > bitems: 158 | return 1 159 | 160 | # compare child nodes 161 | achildren = list(a) 162 | achildren.sort(key=cmp_to_key(xml_tree_compare)) 163 | bchildren = list(b) 164 | bchildren.sort(key=cmp_to_key(xml_tree_compare)) 165 | 166 | for achild, bchild in zip(achildren, bchildren): 167 | cmpval = xml_tree_compare(achild, bchild) 168 | if cmpval < 0: 169 | return -1 170 | elif cmpval > 0: 171 | return 1 172 | 173 | # must be equal 174 | return 0 175 | 176 | def sync_child_error(el, tag, child, infile, err = None): 177 | if err == None: 178 | err = "Unsupported difference" 179 | 180 | el_child = find_child(el, tag) 181 | if el_child == None: 182 | el.append(deepcopy(child)) 183 | elif xml_tree_compare(el_child, child) != 0: 184 | print_file_error_and_exit(infile, child, err + "\n" + 185 | etree.tostring(el_child).decode() + "\n" + 186 | etree.tostring(child).decode()) 187 | 188 | def sync_child(el, tag, child): 189 | if find_child(el, tag) == None: 190 | el.append(deepcopy(child)) 191 | 192 | # Merges /eagle/drawing/settings element 193 | def merge_xml_settings(out_el, in_el, infile): 194 | for child in in_el: 195 | if child.tag == "setting": 196 | if len(child) > 0: 197 | print_file_error_and_exit(infile, child, "Expected empty") 198 | 199 | # find any setting matching attributes 200 | attrs = child.attrib 201 | attrs = { key : None for key, value in attrs.items() } 202 | 203 | found = find_child(out_el, "setting", attrs) 204 | 205 | if found == None: 206 | out_el.append(deepcopy(child)) 207 | else: 208 | # check if elements are equivalent 209 | if xml_tree_compare(found, child) != 0: 210 | print_file_warning(infile, "Incompatible settings \n" + 211 | etree.tostring(found).decode() + "\n" + 212 | etree.tostring(child).decode()) 213 | else: 214 | print_file_error_and_exit(infile, child) 215 | 216 | if len(in_el.attrib) > 0: 217 | print_file_error_and_exit(infile, in_el, "Unexpected attributes") 218 | 219 | # Merges /eagle/drawing/layers element 220 | def merge_xml_layers(out_el, in_el, infile): 221 | # we only ensure that layer exists, layer info differences are ignored. 222 | for child in in_el: 223 | if child.tag == "layer": 224 | if find_child(out_el, "layer", { "number" : child.get("number") }) == None: 225 | out_el.append(deepcopy(child)) 226 | else: 227 | print_file_error_and_exit(infile, child) 228 | 229 | if len(in_el.attrib) > 0: 230 | print_file_error_and_exit(infile, in_el, "Unexpected attributes") 231 | 232 | def offset_and_rotate(x, y, infile): 233 | if infile.rotation == 0: 234 | pass 235 | elif infile.rotation == 90: 236 | tmp = -y 237 | y = x 238 | x = tmp 239 | elif infile.rotation == 180: 240 | x = -x 241 | y = -y 242 | elif infile.rotation == 270: 243 | tmp = y 244 | y = -x 245 | x = tmp 246 | else: 247 | assert False 248 | 249 | x += infile.offsetx 250 | y += infile.offsety 251 | return x, y 252 | 253 | def update_xml_routing_pos(el, xattr, yattr, infile): 254 | x = el.get(xattr) 255 | y = el.get(yattr) 256 | if x == None or y == None: 257 | print_file_error_and_exit(infile, el) 258 | x, y = offset_and_rotate(float(x), float(y), infile) 259 | el.set(xattr, str(x)) 260 | el.set(yattr, str(y)) 261 | 262 | def update_xml_routing_rot(el, rotattr, infile): 263 | rot = el.get(rotattr) 264 | if rot == None or rot == "": 265 | rot = "R0" 266 | m = re.match(r"^([a-zA-Z]*)(\d+)$", rot) 267 | if m == None: 268 | print_file_error_and_exit(infile, el, "Unsupported rotation attribute " + rot) 269 | prefix = m.group(1) 270 | introt = int(m.group(2)) 271 | 272 | # rotate mirrored parts to opposite direction 273 | if "M" in prefix: 274 | introt = (introt - infile.rotation) % 360 275 | else: 276 | introt = (introt + infile.rotation) % 360 277 | rot = prefix + str(introt) 278 | 279 | if el.get(rotattr) == None and rot == "R0": 280 | # no changes needed 281 | return 282 | el.set(rotattr, rot) 283 | 284 | # Updates the elements within the following nodes and all their sub-nodes: 285 | # plain, signal, elements 286 | # This is where the actual position and rotation modifications are made 287 | def update_routing(el, infile): 288 | if el.tag == "wire": 289 | # in plain or signal 290 | update_xml_routing_pos(el, "x1", "y1", infile) 291 | update_xml_routing_pos(el, "x2", "y2", infile) 292 | elif el.tag == "polygon": 293 | # in plain or signal 294 | for child in el: 295 | update_routing(child, infile) # process vertex nodes 296 | elif el.tag == "text": 297 | # in plain 298 | update_xml_routing_pos(el, "x", "y", infile) 299 | update_xml_routing_rot(el, "rot", infile) 300 | elif el.tag == "dimension": 301 | # in plain 302 | update_xml_routing_pos(el, "x1", "y1", infile) 303 | update_xml_routing_pos(el, "x2", "y2", infile) 304 | update_xml_routing_pos(el, "x3", "y3", infile) 305 | elif el.tag == "circle": 306 | # in plain 307 | update_xml_routing_pos(el, "x", "y", infile) 308 | elif el.tag == "rectangle": 309 | # in plain 310 | update_xml_routing_pos(el, "x1", "y1", infile) 311 | update_xml_routing_pos(el, "x2", "y2", infile) 312 | # note that we are ignoring rotation as it will be dealt with by 313 | # changing the positions of the corners of the rectangle 314 | elif el.tag == "frame": 315 | # in plain 316 | update_xml_routing_pos(el, "x1", "y1", infile) 317 | update_xml_routing_pos(el, "x2", "y2", infile) 318 | elif el.tag == "hole": 319 | # in plain 320 | update_xml_routing_pos(el, "x", "y", infile) 321 | elif el.tag == "contactref": 322 | # in signal 323 | pass 324 | elif el.tag == "via": 325 | # in signal 326 | update_xml_routing_pos(el, "x", "y", infile) 327 | elif el.tag == "element": 328 | # in elements 329 | update_xml_routing_pos(el, "x", "y", infile) 330 | update_xml_routing_rot(el, "rot", infile) 331 | for child in el: 332 | update_routing(child, infile) # process attribute or variant nodes 333 | elif el.tag == "vertex": 334 | # in polygon 335 | update_xml_routing_pos(el, "x", "y", infile) 336 | elif el.tag == "attribute": 337 | # in element 338 | update_xml_routing_pos(el, "x", "y", infile) 339 | update_xml_routing_rot(el, "rot", infile) 340 | elif el.tag == "variant": 341 | # in element 342 | pass 343 | else: 344 | print_file_error_and_exit(infile, el) 345 | 346 | # Hides the current name label and adds a custom label that is displayed as if 347 | # the element has old_name. 348 | def override_name_label(el, old_name): 349 | 350 | if el.get("name") == old_name: 351 | return 352 | name_attr = find_child(el, "attribute", { "name" : "NAME" }) 353 | if name_attr == None: 354 | return 355 | name_attr_dup = deepcopy(name_attr) 356 | name_attr_dup.set("name", "NAME1") 357 | name_attr_dup.set("value", old_name) 358 | el.append(name_attr_dup) 359 | name_attr.set("display", "off") 360 | 361 | # Merges /eagle/drawing/board/plain element 362 | def append_xml_plain(out_el, in_el, infile): 363 | 364 | for child in in_el: 365 | new_child = deepcopy(child) 366 | out_el.append(new_child) 367 | update_routing(new_child, infile) 368 | 369 | if len(in_el.attrib) > 0: 370 | print_file_error_and_exit(infile, in_el, "Unexpected attributes") 371 | 372 | # Merges /eagle/drawing/board/libraries/library/packages element 373 | def merge_xml_packages(out_el, in_el, infile): 374 | 375 | for child in in_el: 376 | if child.tag == "package": 377 | out_child = find_child(out_el, "package", { "name" : child.get("name") }) 378 | if out_child == None: 379 | out_el.append(deepcopy(child)) 380 | else: 381 | if xml_tree_compare(out_child, child) != 0: 382 | err = "Embedded libraries contain different packages of the same name {0}\n".format(child.get("name")) 383 | err += etree.tostring(out_child).decode() + "\n" 384 | err += etree.tostring(child).decode() 385 | print_file_error_and_exit(infile, child, err) 386 | else: 387 | print_file_error_and_exit(infile, child) 388 | 389 | if len(in_el.attrib) > 0: 390 | print_file_error_and_exit(infile, in_el, "Unexpected attributes") 391 | 392 | # Merges /eagle/drawing/board/libraries/library element 393 | def merge_xml_library(out_el, in_el, infile): 394 | assert out_el.get("name") == in_el.get("name") 395 | 396 | for child in in_el: 397 | if child.tag == "description": 398 | sync_child(out_el, "description", child) 399 | elif child.tag == "packages": 400 | merge_xml_packages(find_or_create_child(out_el, "packages"), child, infile) 401 | else: 402 | print_file_error_and_exit(infile, child) 403 | 404 | #if len(in_el.attrib) > 0: 405 | # print_file_error_and_exit(infile, in_el, "Unexpected attributes") 406 | 407 | # Merges /eagle/drawing/board/libraries element 408 | def merge_xml_libraries(out_el, in_el, infile): 409 | 410 | for child in in_el: 411 | if child.tag == "library": 412 | out_child = find_child(out_el, "library", { "name" : child.get("name") }) 413 | if out_child == None: 414 | out_el.append(deepcopy(child)) 415 | else: 416 | merge_xml_library(out_child, child, infile) 417 | else: 418 | print_file_error_and_exit(infile, child) 419 | 420 | if len(in_el.attrib) > 0: 421 | print_file_error_and_exit(infile, in_el, "Unexpected attributes") 422 | 423 | # Merges /eagle/drawing/board/elements element 424 | # Eagle requires that real names of elements are not duplicated, thus this 425 | # function ensures that a unique name is used. The label is overridden to 426 | # display the old name by defining a custom attribute. 427 | def append_xml_elements(out_el, in_el, element_map, infile): 428 | 429 | for child in in_el: 430 | if child.tag == "element": 431 | new_child = deepcopy(child) 432 | update_routing(new_child, infile) 433 | 434 | # make sure the name of the new signal is unique 435 | name = new_child.get("name") 436 | prev_name = name 437 | postfix = "" 438 | postfix_num = 1 439 | while find_child(out_el, "element", { "name" : name + postfix }) != None: 440 | postfix = "_" + str(postfix_num) 441 | postfix_num += 1 442 | name = name + postfix 443 | 444 | new_child.set("name", name) 445 | if name != prev_name: 446 | element_map[prev_name] = name 447 | override_name_label(new_child, prev_name) 448 | 449 | out_el.append(new_child) 450 | else: 451 | print_file_error_and_exit(infile, child) 452 | 453 | if len(in_el.attrib) > 0: 454 | print_file_error_and_exit(infile, in_el, "Unexpected attributes") 455 | 456 | def update_signal_element_names(el, element_map): 457 | if el.tag != "contactref": 458 | return 459 | name = el.get("element") 460 | if name != None and name in element_map: 461 | el.set("element", element_map[name]) 462 | 463 | # Merges /eagle/drawing/board/signals element 464 | def append_xml_signals(out_el, in_el, element_map, infile): 465 | 466 | for child in in_el: 467 | if child.tag == "signal": 468 | new_child = deepcopy(child) 469 | for new_child2 in new_child: 470 | update_routing(new_child2, infile) 471 | update_signal_element_names(new_child2, element_map) 472 | 473 | # make sure the name of the new signal is unique 474 | name = new_child.get("name") 475 | postfix = "" 476 | postfix_num = 1 477 | while find_child(out_el, "signal", { "name" : name + postfix }) != None: 478 | postfix = str(postfix_num) 479 | postfix_num += 1 480 | name = name + postfix 481 | 482 | new_child.set("name", name) 483 | out_el.append(new_child) 484 | else: 485 | print_file_error_and_exit(infile, child) 486 | 487 | if len(in_el.attrib) > 0: 488 | print_file_error_and_exit(infile, in_el, "Unexpected attributes") 489 | 490 | # Merges /eagle/drawing/board/errors element 491 | def append_xml_errors(out_el, in_el, infile): 492 | # TODO 493 | pass 494 | 495 | # Merges /eagle/drawing/board element 496 | def merge_xml_board(out_el, in_el, infile): 497 | 498 | # Element names must be unique; this dict stores the old->new name mapping 499 | element_map = {} 500 | 501 | for child in in_el: 502 | if child.tag == "plain": 503 | append_xml_plain(find_or_create_child(out_el, "plain"), child, infile) 504 | elif child.tag == "libraries": 505 | merge_xml_libraries(find_or_create_child(out_el, "libraries"), child, infile) 506 | elif child.tag == "attributes": 507 | sync_child_error(out_el, "attributes", child, infile) # differences not supported 508 | elif child.tag == "variantdefs": 509 | sync_child_error(out_el, "variantdefs", child, infile) # differences not supported 510 | elif child.tag == "classes": 511 | sync_child_error(out_el, "classes", child, infile) # differences not supported 512 | elif child.tag == "designrules": 513 | # ensure that design rule info is equivalent 514 | sync_child_error(out_el, "designrules", child, infile, "Design rules must be equivalent") 515 | elif child.tag == "autorouter": 516 | # we just take the autorouter element of the first file it exists 517 | sync_child(out_el, "autorouter", child) 518 | elif child.tag == "elements": 519 | append_xml_elements(find_or_create_child(out_el, "elements"), child, element_map, infile) 520 | elif child.tag == "signals": 521 | append_xml_signals(find_or_create_child(out_el, "signals"), child, element_map, infile) 522 | elif child.tag == "errors": 523 | append_xml_errors(find_or_create_child(out_el, "errors"), child, infile) 524 | else: 525 | print_file_error_and_exit(infile, child) 526 | 527 | if len(in_el.attrib) > 0: 528 | print_file_error_and_exit(infile, in_el, "Unexpected attributes") 529 | 530 | # Merges /eagle/drawing element 531 | def merge_xml_drawing(out_el, in_el, infile): 532 | 533 | for child in in_el: 534 | if child.tag == "settings": 535 | merge_xml_settings(find_or_create_child(out_el, "settings"), child, infile) 536 | elif child.tag == "grid": 537 | # we just take the grid element of the first file 538 | sync_child(out_el, "grid", child) 539 | elif child.tag == "layers": 540 | merge_xml_layers(find_or_create_child(out_el, "layers"), child, infile) 541 | elif child.tag == "board": 542 | merge_xml_board(find_or_create_child(out_el, "board"), child, infile) 543 | else: 544 | print_file_error_and_exit(infile, child) 545 | 546 | if len(in_el.attrib) > 0: 547 | print_file_error_and_exit(infile, in_el, "Unexpected attributes") 548 | 549 | # Merges /eagle element 550 | def merge_xml_eagle(out_el, in_el, infile): 551 | assert out_el.tag == "eagle" and in_el.tag == "eagle" 552 | 553 | in_attrs = in_el.attrib 554 | for attr in in_attrs: 555 | if attr == "version": 556 | in_version = in_el.get("version") 557 | out_version = out_el.get("version") 558 | if out_version == None: 559 | out_el.set("version", in_version) 560 | else: 561 | if in_version != out_version: 562 | print_file_error_and_exit(infile, in_el, 563 | "Eagle version mismatch: {0} != {1}".format(in_version, out_version)) 564 | else: 565 | print_file_error_and_exit(infile, in_el, "Unexpected attributes") 566 | 567 | 568 | for child in in_el: 569 | if child.tag == "drawing": 570 | merge_xml_drawing(find_or_create_child(out_el, "drawing"), child, infile) 571 | elif child.tag == "compatibility": 572 | print_file_warning(infile, "Compatibility notes ignored") 573 | 574 | def main(): 575 | outfile, infiles = parse_args() 576 | 577 | out_el = etree.Element("eagle") 578 | for infile in infiles: 579 | if not os.path.exists(infile.path): 580 | print("Input file {0} not found!".format(infile.path)) 581 | sys.exit(1) 582 | 583 | in_el = etree.parse(infile.path) 584 | merge_xml_eagle(out_el, in_el.getroot(), infile) 585 | 586 | open(outfile, "wb").write(etree.tostring(out_el, xml_declaration=True, 587 | encoding="UTF-8", 588 | doctype="")) 589 | 590 | if __name__ == "__main__": 591 | main() 592 | -------------------------------------------------------------------------------- /test/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ../merge.py test_out.brd \ 4 | test_in.brd \ 5 | test_in.brd --offx 100mm --rotation 90 \ 6 | test_in.brd --offx 100mm --offy 100mm --rotation 180 \ 7 | test_in.brd --offy 100mm --rotation 270 8 | --------------------------------------------------------------------------------