kopia lustrzana https://gitlab.com/gerbolyze/gerbonara
550 wiersze
21 KiB
Python
550 wiersze
21 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright 2022 Jan Sebastian Götte <gerbonara@jaseg.de>
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
# Based on https://github.com/tracespace/tracespace
|
|
#
|
|
|
|
import math
|
|
|
|
from PIL import Image
|
|
import pytest
|
|
|
|
from ..rs274x import GerberFile
|
|
from ..cam import FileSettings
|
|
|
|
from .image_support import *
|
|
from .utils import *
|
|
|
|
REFERENCE_FILES = [ l.strip() for l in '''
|
|
board_outline.GKO
|
|
example_outline_with_arcs.gbr
|
|
example_two_square_boxes.gbr
|
|
example_coincident_hole.gbr
|
|
example_cutin.gbr
|
|
example_cutin_multiple.gbr
|
|
example_flash_circle.gbr
|
|
example_flash_obround.gbr
|
|
example_flash_polygon.gbr
|
|
example_flash_rectangle.gbr
|
|
example_fully_coincident.gbr
|
|
example_guess_by_content.g0
|
|
example_holes_dont_clear.gbr
|
|
example_level_holes.gbr
|
|
example_not_overlapping_contour.gbr
|
|
example_not_overlapping_touching.gbr
|
|
example_overlapping_contour.gbr
|
|
example_overlapping_touching.gbr
|
|
example_simple_contour.gbr
|
|
example_single_contour_1.gbr
|
|
example_single_contour_2.gbr
|
|
example_single_contour_3.gbr
|
|
example_am_exposure_modifier.gbr
|
|
bottom_copper.GBL
|
|
bottom_mask.GBS
|
|
bottom_silk.GBO
|
|
eagle_files/copper_bottom_l4.gbr
|
|
eagle_files/copper_inner_l2.gbr
|
|
eagle_files/copper_inner_l3.gbr
|
|
eagle_files/copper_top_l1.gbr
|
|
eagle_files/profile.gbr
|
|
eagle_files/silkscreen_bottom.gbr
|
|
eagle_files/silkscreen_top.gbr
|
|
eagle_files/soldermask_bottom.gbr
|
|
eagle_files/soldermask_top.gbr
|
|
eagle_files/solderpaste_bottom.gbr
|
|
eagle_files/solderpaste_top.gbr
|
|
multiline_read.ger
|
|
test_fine_lines_x.gbr
|
|
test_fine_lines_y.gbr
|
|
top_copper.GTL
|
|
top_mask.GTS
|
|
top_silk.GTO
|
|
open_outline_altium.gbr
|
|
easyeda/Gerber_TopSolderMaskLayer.GTS
|
|
easyeda/Gerber_TopSilkLayer.GTO
|
|
easyeda/Gerber_BottomSolderMaskLayer.GBS
|
|
easyeda/Gerber_BoardOutline.GKO
|
|
easyeda/Gerber_TopLayer.GTL
|
|
easyeda/Gerber_BottomLayer.GBL
|
|
easyeda/Gerber_TopPasteMaskLayer.GTP
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_lyr2.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_lyr3.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_fab.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_lyr10_GAF.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_lyr7.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_sps.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_lyr6.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_lyr1_GAF.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_assy.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_smc_GAF.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_lyr4.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_lyr5.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_bslk.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_spc.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_tslk_GAF.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_lyr8.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_sms_GAF.art
|
|
allegro-2/MinnowMax_RevA1_GAF_Gerber/MinnowMax_lyr9.art
|
|
eagle-newer/solderpaste_bottom.gbr
|
|
eagle-newer/silkscreen_bottom.gbr
|
|
eagle-newer/profile.gbr
|
|
eagle-newer/copper_bottom.gbr
|
|
eagle-newer/soldermask_top.gbr
|
|
eagle-newer/solderpaste_top.gbr
|
|
eagle-newer/soldermask_bottom.gbr
|
|
eagle-newer/silkscreen_top.gbr
|
|
eagle-newer/copper_top.gbr
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G4
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G9
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GBL
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GTO
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G11
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G1
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GBP
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G2
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GM15
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GTS
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G6
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G7
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G3
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GPB
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GM1
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G12
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GBS
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GTL
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G10
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GM14
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G5
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GTP
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GBO
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.G8
|
|
altium-composite-drill/Gerber/LimeSDR-QPCIe_1v2.GPT
|
|
geda/driver.topmask.gbr
|
|
geda/controller.top.gbr
|
|
geda/controller.bottom.gbr
|
|
geda/driver.bottommask.gbr
|
|
geda/driver.top.gbr
|
|
geda/driver.bottom.gbr
|
|
geda/controller.topsilk.gbr
|
|
geda/controller.fab.gbr
|
|
geda/driver.topsilk.gbr
|
|
geda/controller.group3.gbr
|
|
geda/controller.topmask.gbr
|
|
geda/driver.group5.gbr
|
|
geda/controller.bottommask.gbr
|
|
geda/driver.fab.gbr
|
|
pcb-rnd/power-art.gko
|
|
pcb-rnd/power-art.ast
|
|
pcb-rnd/power-art.gtl
|
|
pcb-rnd/power-art.gto
|
|
pcb-rnd/power-art.gtp
|
|
pcb-rnd/power-art.asb
|
|
pcb-rnd/power-art.gbp
|
|
pcb-rnd/power-art.gbs
|
|
pcb-rnd/power-art.gbl
|
|
pcb-rnd/power-art.fab
|
|
pcb-rnd/power-art.gbo
|
|
pcb-rnd/power-art.gts
|
|
siemens/80101_0125_F200_L04.gdo
|
|
siemens/80101_0125_F200_L12_Bottom.gdo
|
|
siemens/80101_0125_F200_L11.gdo
|
|
siemens/80101_0125_F200_L10.gdo
|
|
siemens/80101_0125_F200_SolderPasteTop.gdo
|
|
siemens/80101_0125_F200_SoldermaskTop.gdo
|
|
siemens/80101_0125_F200_L06.gdo
|
|
siemens/80101_0125_F200_L02.gdo
|
|
siemens/80101_0125_F200_SilkscreenBottom.gdo
|
|
siemens/80101_0125_F200_SoldermaskBottom.gdo
|
|
siemens/80101_0125_F200_SolderPasteBottom.gdo
|
|
siemens/80101_0125_F200_L03.gdo
|
|
siemens/80101_0125_F200_L01_Top.gdo
|
|
Target3001/IRNASIoTbank1.2.Bot
|
|
Target3001/IRNASIoTbank1.2.Outline
|
|
Target3001/IRNASIoTbank1.2.PasteBot
|
|
Target3001/IRNASIoTbank1.2.PasteTop
|
|
Target3001/IRNASIoTbank1.2.PosiBot
|
|
Target3001/IRNASIoTbank1.2.PosiTop
|
|
Target3001/IRNASIoTbank1.2.StopBot
|
|
Target3001/IRNASIoTbank1.2.StopTop
|
|
Target3001/IRNASIoTbank1.2.Top
|
|
kicad-older/chibi_2024-Edge.Cuts.gbr
|
|
kicad-older/chibi_2024-F.SilkS.gbr
|
|
kicad-older/chibi_2024-B.Paste.gbr
|
|
kicad-older/chibi_2024-B.Cu.gbr
|
|
kicad-older/chibi_2024-F.Mask.gbr
|
|
kicad-older/chibi_2024-B.Mask.gbr
|
|
kicad-older/chibi_2024-F.Paste.gbr
|
|
kicad-older/chibi_2024-B.SilkS.gbr
|
|
kicad-older/chibi_2024-F.Cu.gbr
|
|
fritzing/combined.gbs
|
|
fritzing/combined.gm1
|
|
fritzing/combined.gbl
|
|
fritzing/combined.gbo
|
|
fritzing/combined.GKO
|
|
fritzing/combined.gtl
|
|
fritzing/combined.gts
|
|
fritzing/combined.gto
|
|
siemens-2/Gerber/SoldermaskTop.gdo
|
|
siemens-2/Gerber/EtchLayerTop.gdo
|
|
siemens-2/Gerber/DrillDrawingThrough.gdo
|
|
siemens-2/Gerber/SoldermaskBottom.gdo
|
|
siemens-2/Gerber/SolderPasteBottom.gdo
|
|
siemens-2/Gerber/SolderPasteTop.gdo
|
|
siemens-2/Gerber/EtchLayerBottom.gdo
|
|
siemens-2/Gerber/BoardOutlline.gdo
|
|
upverter/design_export.gko
|
|
upverter/design_export.gtl
|
|
upverter/design_export.gbp
|
|
upverter/design_export.gtp
|
|
upverter/design_export.gbl
|
|
upverter/design_export.gto
|
|
upverter/design_export.gbs
|
|
upverter/design_export.gts
|
|
upverter/design_export.gbo
|
|
eagle_files/solderpaste_bottom.gbr
|
|
eagle_files/silkscreen_bottom.gbr
|
|
eagle_files/profile.gbr
|
|
eagle_files/copper_inner_l2.gbr
|
|
eagle_files/copper_top_l1.gbr
|
|
eagle_files/soldermask_top.gbr
|
|
eagle_files/copper_inner_l3.gbr
|
|
eagle_files/solderpaste_top.gbr
|
|
eagle_files/soldermask_bottom.gbr
|
|
eagle_files/copper_bottom_l4.gbr
|
|
eagle_files/silkscreen_top.gbr
|
|
diptrace/panel_BoardOutline.gbr
|
|
diptrace/keyboard_BottomSilk.gbr
|
|
diptrace/keyboard_Bottom.gbr
|
|
diptrace/mainboard_Top.gbr
|
|
diptrace/mainboard_TopMask.gbr
|
|
diptrace/mainboard_BoardOutline.gbr
|
|
diptrace/mainboard_Bottom.gbr
|
|
diptrace/mainboard_BottomMask.gbr
|
|
diptrace/keyboard_BottomMask.gbr
|
|
diptrace/panel_Bottom.gbr
|
|
diptrace/keyboard_BoardOutline.gbr
|
|
diptrace/panel_BottomSilk.gbr
|
|
diptrace/panel_BottomMask.gbr
|
|
diptrace/mainboard_TopSilk.gbr
|
|
zuken-emulated/Gerber/MetalMask-A.fph
|
|
zuken-emulated/Gerber/MetalMask-B.fph
|
|
zuken-emulated/Gerber/Symbol-A.fph
|
|
zuken-emulated/Gerber/Symbol-B.fph
|
|
zuken-emulated/Gerber/Resist-A.fph
|
|
zuken-emulated/Gerber/Resist-B.fph
|
|
zuken-emulated/Gerber/Conductive-1.fph
|
|
zuken-emulated/Gerber/Conductive-2.fph
|
|
'''.splitlines() if l ]
|
|
|
|
MIN_REFERENCE_FILES = [
|
|
'example_two_square_boxes.gbr',
|
|
'example_outline_with_arcs.gbr',
|
|
'example_flash_circle.gbr',
|
|
'example_flash_polygon.gbr',
|
|
'example_flash_rectangle.gbr',
|
|
'example_simple_contour.gbr',
|
|
'example_am_exposure_modifier.gbr',
|
|
'bottom_copper.GBL',
|
|
'bottom_silk.GBO',
|
|
'eagle_files/copper_bottom_l4.gbr'
|
|
]
|
|
|
|
HAS_ZERO_SIZE_APERTURES = [
|
|
'bottom_copper.GBL',
|
|
'bottom_silk.GBO',
|
|
'top_copper.GTL',
|
|
'top_silk.GTO',
|
|
'board_outline.GKO',
|
|
'silkscreen_top.gbr',
|
|
'combined.GKO',
|
|
'combined.gto',
|
|
'EtchLayerTop.gdo',
|
|
'EtchLayerBottom.gdo',
|
|
'BoardOutlline.gdo',
|
|
]
|
|
|
|
|
|
@filter_syntax_warnings
|
|
@pytest.mark.parametrize('reference', REFERENCE_FILES, indirect=True)
|
|
def test_round_trip(reference, tmpfile):
|
|
tmp_gbr = tmpfile('Output gerber', '.gbr')
|
|
|
|
GerberFile.open(reference).save(tmp_gbr)
|
|
|
|
mean, _max, hist = gerber_difference(reference, tmp_gbr, diff_out=tmpfile('Difference', '.png'))
|
|
assert mean < 5e-5
|
|
assert hist[9] == 0
|
|
assert hist[3:].sum() < 5e-5*hist.size
|
|
|
|
@filter_syntax_warnings
|
|
@pytest.mark.parametrize('reference', REFERENCE_FILES, indirect=True)
|
|
def test_idempotence(reference, tmpfile):
|
|
tmp_gbr_1 = tmpfile('First generation output', '.gbr')
|
|
tmp_gbr_2 = tmpfile('Second generation output', '.gbr')
|
|
|
|
GerberFile.open(reference).save(tmp_gbr_1)
|
|
GerberFile.open(tmp_gbr_1).save(tmp_gbr_2)
|
|
assert tmp_gbr_1.read_text() == tmp_gbr_2.read_text()
|
|
|
|
|
|
TEST_ANGLES = [90, 180, 270, 1.5, 30, 360, 1024, -30]
|
|
TEST_OFFSETS = [(0, 0), (100, 0), (0, 100), (2, 0), (10, 100)]
|
|
|
|
@filter_syntax_warnings
|
|
@pytest.mark.parametrize('reference', MIN_REFERENCE_FILES, indirect=True)
|
|
@pytest.mark.parametrize('angle', TEST_ANGLES)
|
|
def test_rotation(reference, angle, tmpfile):
|
|
if 'flash_rectangle' in str(reference) and angle == 1024:
|
|
# gerbv's rendering of this is broken, the hole is missing.
|
|
pytest.skip()
|
|
|
|
tmp_gbr = tmpfile('Output gerber', '.gbr')
|
|
|
|
f = GerberFile.open(reference)
|
|
f.rotate(math.radians(angle))
|
|
f.save(tmp_gbr)
|
|
|
|
cx, cy = 0, to_gerbv_svg_units(10, unit='inch')
|
|
mean, _max, hist = gerber_difference(reference, tmp_gbr, diff_out=tmpfile('Difference', '.png'),
|
|
svg_transform=f'rotate({angle} {cx} {cy})')
|
|
assert mean < 1e-3 # relax mean criterion compared to above.
|
|
assert hist[9] == 0
|
|
|
|
@filter_syntax_warnings
|
|
@pytest.mark.parametrize('reference', MIN_REFERENCE_FILES, indirect=True)
|
|
@pytest.mark.parametrize('angle', TEST_ANGLES)
|
|
@pytest.mark.parametrize('center', [(0, 0), (10, 0), (0, -10), (10, 20)])
|
|
def test_rotation_center(reference, angle, center, tmpfile):
|
|
if 'flash_rectangle' in str(reference) and angle in (30, 1024):
|
|
# gerbv's rendering of this is broken, the hole is missing.
|
|
pytest.skip()
|
|
|
|
tmp_gbr = tmpfile('Output gerber', '.gbr')
|
|
|
|
f = GerberFile.open(reference)
|
|
f.rotate(math.radians(angle), center=center)
|
|
f.save(tmp_gbr)
|
|
|
|
# calculate circle center in SVG coordinates
|
|
size = (10, 10) # inches
|
|
cx, cy = to_gerbv_svg_units(center[0]), to_gerbv_svg_units(size[1], 'inch')-to_gerbv_svg_units(center[1], 'mm')
|
|
mean, _max, hist = gerber_difference(reference, tmp_gbr, diff_out=tmpfile('Difference', '.png'),
|
|
svg_transform=f'rotate({angle} {cx} {cy})',
|
|
size=size)
|
|
assert mean < 1e-3
|
|
assert hist[9] < 50
|
|
assert hist[3:].sum() < 1e-3*hist.size
|
|
|
|
@filter_syntax_warnings
|
|
@pytest.mark.parametrize('reference', MIN_REFERENCE_FILES, indirect=True)
|
|
@pytest.mark.parametrize('offset', TEST_OFFSETS)
|
|
def test_offset(reference, offset, tmpfile):
|
|
tmp_gbr = tmpfile('Output gerber', '.gbr')
|
|
|
|
f = GerberFile.open(reference)
|
|
f.offset(*offset)
|
|
f.save(tmp_gbr, settings=FileSettings(unit=f.unit, number_format=(4,7)))
|
|
|
|
# flip y offset since svg's y axis is flipped compared to that of gerber
|
|
dx, dy = to_gerbv_svg_units(offset[0]), -to_gerbv_svg_units(offset[1])
|
|
mean, _max, hist = gerber_difference(reference, tmp_gbr, diff_out=tmpfile('Difference', '.png'),
|
|
svg_transform=f'translate({dx} {dy})')
|
|
assert mean < 1e-4
|
|
assert hist[9] == 0
|
|
|
|
@filter_syntax_warnings
|
|
@pytest.mark.parametrize('reference', MIN_REFERENCE_FILES, indirect=True)
|
|
@pytest.mark.parametrize('angle', TEST_ANGLES)
|
|
@pytest.mark.parametrize('center', [(0, 0), (10, 0), (0, -10), (10, 20)])
|
|
@pytest.mark.parametrize('offset', [(0, 0), (100, 0), (0, 100), (100, 10)])
|
|
def test_combined(reference, angle, center, offset, tmpfile):
|
|
if 'flash_rectangle' in str(reference) and angle in (30, 1024):
|
|
# gerbv's rendering of this is broken, the hole is missing.
|
|
pytest.skip()
|
|
|
|
tmp_gbr = tmpfile('Output gerber', '.gbr')
|
|
|
|
f = GerberFile.open(reference)
|
|
f.rotate(math.radians(angle), center=center)
|
|
f.offset(*offset)
|
|
f.save(tmp_gbr, settings=FileSettings(unit=f.unit, number_format=(4,7)))
|
|
|
|
size = (10, 10) # inches
|
|
cx, cy = to_gerbv_svg_units(center[0]), to_gerbv_svg_units(size[1], 'inch')-to_gerbv_svg_units(center[1], 'mm')
|
|
dx, dy = to_gerbv_svg_units(offset[0]), -to_gerbv_svg_units(offset[1])
|
|
mean, _max, hist = gerber_difference(reference, tmp_gbr, diff_out=tmpfile('Difference', '.png'),
|
|
svg_transform=f'translate({dx} {dy}) rotate({angle} {cx} {cy})',
|
|
size=size)
|
|
assert mean < 1e-3
|
|
assert hist[9] < 100
|
|
assert hist[3:].sum() < 1e-3*hist.size
|
|
|
|
@filter_syntax_warnings
|
|
@pytest.mark.parametrize('file_a', MIN_REFERENCE_FILES)
|
|
@pytest.mark.parametrize('file_b', [
|
|
'example_two_square_boxes.gbr',
|
|
'example_outline_with_arcs.gbr',
|
|
'example_am_exposure_modifier.gbr',
|
|
'bottom_silk.GBO',
|
|
'eagle_files/copper_bottom_l4.gbr', ])
|
|
@pytest.mark.parametrize('angle', [0, 10, 90])
|
|
@pytest.mark.parametrize('offset', [(0, 0, 0, 0), (100, 0, 0, 0), (0, 0, 0, 100), (100, 0, 0, 100)])
|
|
def test_compositing(file_a, file_b, angle, offset, tmpfile, print_on_error):
|
|
|
|
# TODO bottom_silk.GBO renders incorrectly with gerbv: the outline does not exist in svg. In GUI, the logo only
|
|
# renders at very high magnification. Skip, and once we have our own SVG export maybe use that instead. Or just use
|
|
# KiCAD's gerbview.
|
|
# TODO check if this and the issue with aperture holes not rendering in test_combined actually are bugs in gerbv
|
|
# and fix/report upstream.
|
|
if file_a == 'bottom_silk.GBO' or file_b == 'bottom_silk.GBO':
|
|
pytest.skip()
|
|
|
|
ref_a = reference_path(file_a)
|
|
print_on_error('Reference file a:', ref_a)
|
|
ref_b = reference_path(file_b)
|
|
print_on_error('Reference file b:', ref_b)
|
|
|
|
ax, ay, bx, by = offset
|
|
grb_a = GerberFile.open(ref_a)
|
|
grb_a.rotate(math.radians(angle))
|
|
grb_a.offset(ax, ay)
|
|
|
|
grb_b = GerberFile.open(ref_b)
|
|
grb_b.offset(bx, by)
|
|
|
|
grb_a.merge(grb_b)
|
|
tmp_gbr = tmpfile('Output gerber', '.gbr')
|
|
grb_a.save(tmp_gbr, settings=FileSettings(unit=grb_a.unit, number_format=(4,7)))
|
|
|
|
size = (10, 10) # inches
|
|
ax, ay = to_gerbv_svg_units(ax), -to_gerbv_svg_units(ay)
|
|
bx, by = to_gerbv_svg_units(bx), -to_gerbv_svg_units(by)
|
|
# note that we have to specify cx, cy even if we rotate around the origin since gerber's origin lies at (x=0
|
|
# y=+document size) in SVG's coordinate space because svg's y axis is flipped compared to gerber's.
|
|
cx, cy = 0, to_gerbv_svg_units(size[1], 'inch')
|
|
mean, _max, hist = gerber_difference_merge(ref_a, ref_b, tmp_gbr,
|
|
composite_out=tmpfile('Composite', '.svg'), diff_out=tmpfile('Difference', '.png'),
|
|
svg_transform1=f'translate({ax} {ay}) rotate({angle} {cx} {cy})',
|
|
svg_transform2=f'translate({bx} {by})',
|
|
size=size)
|
|
assert mean < 1e-3
|
|
assert hist[9] < 100
|
|
assert hist[3:].sum() < 1e-3*hist.size
|
|
|
|
@filter_syntax_warnings
|
|
@pytest.mark.parametrize('reference', REFERENCE_FILES, indirect=True)
|
|
def test_svg_export_gerber(reference, tmpfile):
|
|
if reference.name in ('silkscreen_bottom.gbr', 'silkscreen_top.gbr', 'top_silk.GTO'):
|
|
# Some weird svg rendering artifact. Might be caused by mismatching svg units between gerbv and us. Result looks
|
|
# fine though.
|
|
pytest.skip()
|
|
|
|
if reference.name == 'MinnowMax_assy.art':
|
|
# This leads to worst-case performance in resvg, this testcase takes over 1h to finish. So skip.
|
|
pytest.skip()
|
|
|
|
grb = GerberFile.open(reference)
|
|
|
|
bounds = (0.0, 0.0), (6.0, 6.0) # bottom left, top right
|
|
|
|
out_svg = tmpfile('Output', '.svg')
|
|
with open(out_svg, 'w') as f:
|
|
f.write(str(grb.to_svg(force_bounds=bounds, arg_unit='inch', fg='black', bg='white')))
|
|
|
|
# NOTE: Instead of having gerbv directly export a PNG, we ask gerbv to output SVG which we then rasterize using
|
|
# resvg. We have to do this since gerbv's built-in cairo-based PNG export has severe aliasing issues. In contrast,
|
|
# using resvg for both allows an apples-to-apples comparison of both results.
|
|
ref_svg = tmpfile('Reference export', '.svg')
|
|
ref_png = tmpfile('Reference render', '.png')
|
|
gerbv_export(reference, ref_svg, origin=bounds[0], size=bounds[1], fg='#000000', bg='#ffffff')
|
|
with svg_soup(ref_svg) as soup:
|
|
cleanup_gerbv_svg(soup)
|
|
svg_to_png(ref_svg, ref_png, dpi=300, bg='white')
|
|
|
|
out_png = tmpfile('Output render', '.png')
|
|
svg_to_png(out_svg, out_png, dpi=300, bg='white')
|
|
|
|
if reference.name in HAS_ZERO_SIZE_APERTURES:
|
|
# gerbv does not render these correctly.
|
|
return
|
|
|
|
mean, _max, hist = image_difference(ref_png, out_png, diff_out=tmpfile('Difference', '.png'))
|
|
assert hist[9] < 1
|
|
if 'Minnow' in reference.name or 'LimeSDR' in reference.name or '80101_0125_F200' in reference.name:
|
|
# This is a dense design with lots of traces, leading to lots of aliasing artifacts.
|
|
assert mean < 10e-3
|
|
assert hist[4:].sum() < 1e-2*hist.size
|
|
else:
|
|
assert mean < 1.2e-3
|
|
assert hist[3:].sum() < 1e-3*hist.size
|
|
|
|
# FIXME test svg margin, bounding box computation
|
|
|
|
@filter_syntax_warnings
|
|
@pytest.mark.parametrize('reference', REFERENCE_FILES, indirect=True)
|
|
def test_bounding_box(reference, tmpfile):
|
|
if reference.name == 'MinnowMax_assy.art':
|
|
# This leads to worst-case performance in resvg, this testcase takes over 1h to finish. So skip.
|
|
pytest.skip()
|
|
# skip this check on files that contain lines with a zero-size aperture at the board edge
|
|
if any(reference.match(f'*/{f}') for f in HAS_ZERO_SIZE_APERTURES):
|
|
pytest.skip()
|
|
|
|
margin = 1.0 # inch
|
|
dpi = 200
|
|
margin_px = int(dpi*margin) # intentionally round down to avoid aliasing artifacts
|
|
|
|
grb = GerberFile.open(reference)
|
|
|
|
if grb.is_empty:
|
|
pytest.skip()
|
|
|
|
out_svg = tmpfile('Output', '.svg')
|
|
with open(out_svg, 'w') as f:
|
|
f.write(str(grb.to_svg(margin=margin, arg_unit='inch', fg='white', bg='black')))
|
|
|
|
out_png = tmpfile('Render', '.png')
|
|
svg_to_png(out_svg, out_png, dpi=dpi)
|
|
|
|
img = np.array(Image.open(out_png))
|
|
img = img[:, :, :3].mean(axis=2) # drop alpha and convert to grayscale
|
|
img = np.round(img).astype(int) # convert to int
|
|
assert (img > 0).any() # there must be some content, none of the test gerbers are completely empty.
|
|
cols = img.sum(axis=1)
|
|
rows = img.sum(axis=0)
|
|
col_prefix, col_suffix = np.argmax(cols > 0), np.argmax(cols[::-1] > 0)
|
|
row_prefix, row_suffix = np.argmax(rows > 0), np.argmax(rows[::-1] > 0)
|
|
print('cols:', col_prefix, col_suffix)
|
|
print('rows:', row_prefix, row_suffix)
|
|
|
|
# Check that all margins are completely black and that the content touches the margins. Allow for some tolerance to
|
|
# allow for antialiasing artifacts and for things like very thin features.
|
|
assert margin_px-2 <= col_prefix <= margin_px+2
|
|
assert margin_px-2 <= col_suffix <= margin_px+2
|
|
assert margin_px-2 <= row_prefix <= margin_px+2
|
|
assert margin_px-2 <= row_suffix <= margin_px+2
|
|
|
|
@filter_syntax_warnings
|
|
def test_syntax_error():
|
|
ref = reference_path('test_syntax_error.gbr')
|
|
with pytest.raises(SyntaxError) as exc_info:
|
|
GerberFile.open(ref)
|
|
|
|
assert 'test_syntax_error.gbr' in exc_info.value.msg
|
|
assert '7' in exc_info.value.msg # lineno
|
|
|