From f46b8897818439269d3fbce32773ec1ed12ad657 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 1 Jan 2022 16:28:49 +0100 Subject: Merge works. --- gerbonara/gerber/tests/image_support.py | 57 +++++++++++++++++++-- gerbonara/gerber/tests/test_rs274x.py | 88 ++++++++++++++++++++++++++++----- 2 files changed, 130 insertions(+), 15 deletions(-) (limited to 'gerbonara/gerber/tests') diff --git a/gerbonara/gerber/tests/image_support.py b/gerbonara/gerber/tests/image_support.py index 23b829d..9c80eec 100644 --- a/gerbonara/gerber/tests/image_support.py +++ b/gerbonara/gerber/tests/image_support.py @@ -109,12 +109,61 @@ def gerber_difference(reference, actual, diff_out=None, svg_transform=None, size with svg_soup(act_svg.name) as soup: cleanup_clips(soup) - # FIXME DEBUG - shutil.copyfile(act_svg.name, '/tmp/test-act.svg') - shutil.copyfile(ref_svg.name, '/tmp/test-ref.svg') - return svg_difference(ref_svg.name, act_svg.name, diff_out=diff_out) +def gerber_difference_merge(ref1, ref2, actual, diff_out=None, composite_out=None, svg_transform1=None, svg_transform2=None, size=(10,10)): + with tempfile.NamedTemporaryFile(suffix='.svg') as act_svg,\ + tempfile.NamedTemporaryFile(suffix='.svg') as ref1_svg,\ + tempfile.NamedTemporaryFile(suffix='.svg') as ref2_svg: + + gbr_to_svg(ref1, ref1_svg.name, size=size) + gbr_to_svg(ref2, ref2_svg.name, size=size) + gbr_to_svg(actual, act_svg.name, size=size) + + with svg_soup(ref1_svg.name) as soup1: + if svg_transform1 is not None: + soup1.find('g', attrs={'id': 'surface1'})['transform'] = svg_transform1 + cleanup_clips(soup1) + + with svg_soup(ref2_svg.name) as soup2: + if svg_transform2 is not None: + soup2.find('g', attrs={'id': 'surface1'})['transform'] = svg_transform2 + cleanup_clips(soup2) + + defs1 = soup1.find('defs') + if not defs1: + defs1 = soup1.new_tag('defs') + soup1.find('svg').insert(0, defs1) + + defs2 = soup2.find('defs') + if defs2: + defs2 = defs2.extract() + # explicitly convert .contents into list here and below because else bs4 stumbles over itself + # iterating because we modify the tree in the loop body. + for c in list(defs2.contents): + if hasattr(c, 'attrs'): + c['id'] = 'gn-merge-b-' + c.attrs.get('id', str(id(c))) + defs1.append(c) + + for use in soup2.find_all('use', recursive=True): + if (href := use.get('xlink:href', '')).startswith('#'): + use['xlink:href'] = f'#gn-merge-b-{href[1:]}' + + svg1 = soup1.find('svg') + for c in list(soup2.find('svg').contents): + if hasattr(c, 'attrs'): + c['id'] = 'gn-merge-b-' + c.attrs.get('id', str(id(c))) + svg1.append(c) + # FIXME prefix all group ids with "b-" + + if composite_out: + shutil.copyfile(ref1_svg.name, composite_out) + + with svg_soup(act_svg.name) as soup: + cleanup_clips(soup) + + return svg_difference(ref1_svg.name, act_svg.name, diff_out=diff_out) + def svg_difference(reference, actual, diff_out=None): with tempfile.NamedTemporaryFile(suffix='-ref.png') as ref_png,\ tempfile.NamedTemporaryFile(suffix='-act.png') as act_png: diff --git a/gerbonara/gerber/tests/test_rs274x.py b/gerbonara/gerber/tests/test_rs274x.py index 0b061b1..90ccdb9 100644 --- a/gerbonara/gerber/tests/test_rs274x.py +++ b/gerbonara/gerber/tests/test_rs274x.py @@ -17,7 +17,7 @@ import pytest from ..rs274x import GerberFile from ..cam import FileSettings -from .image_support import gerber_difference +from .image_support import gerber_difference, gerber_difference_merge deg_to_rad = lambda a: a/180 * math.pi @@ -28,9 +28,10 @@ reference_path = lambda reference: Path(__file__).parent / 'resources' / referen @pytest.fixture def temp_files(request): with tempfile.NamedTemporaryFile(suffix='.gbr') as tmp_out_gbr,\ + tempfile.NamedTemporaryFile(suffix='.svg') as tmp_out_svg,\ tempfile.NamedTemporaryFile(suffix='.png') as tmp_out_png: - yield Path(tmp_out_gbr.name), Path(tmp_out_png.name) + yield Path(tmp_out_gbr.name), Path(tmp_out_svg.name), Path(tmp_out_png.name) if request.node.rep_call.failed: module, _, test_name = request.node.nodeid.rpartition('::') @@ -39,14 +40,27 @@ def temp_files(request): test_name = re.sub(r'[^\w\d]', '_', test_name) fail_dir.mkdir(exist_ok=True) perm_path_gbr = fail_dir / f'failure_{test_name}.gbr' + perm_path_svg = fail_dir / f'failure_{test_name}.svg' perm_path_png = fail_dir / f'failure_{test_name}.png' shutil.copy(tmp_out_gbr.name, perm_path_gbr) + if Path(tmp_out_svg.name).is_file(): + shutil.copy(tmp_out_svg.name, perm_path_svg) shutil.copy(tmp_out_png.name, perm_path_png) print(f'Failing output saved to {perm_path_gbr}') - print(f'Reference file is {reference_path(request.node.funcargs["reference"])}') + args = request.node.funcargs + if 'reference' in args: + print(f'Reference file is {reference_path(args["reference"])}') + else: + print(f'Reference file A is {reference_path(args["file_a"])}') + print(f'Reference file B is {reference_path(args["file_b"])}') print(f'Difference image saved to {perm_path_png}') + if Path(tmp_out_svg.name).is_file(): + print(f'Sum SVG saved to {perm_path_svg}') print(f'gerbv command line:') - print(f'gerbv {perm_path_gbr} {reference_path(request.node.funcargs["reference"])}') + if 'reference' in args: + print(f'gerbv {perm_path_gbr} {reference_path(request.node.funcargs["reference"])}') + else: + print(f'gerbv {perm_path_gbr} {reference_path(args["file_a"])} {reference_path(args["file_b"])}') to_gerbv_svg_units = lambda val, unit='mm': val*72 if unit == 'inch' else val/25.4*72 @@ -109,11 +123,12 @@ MIN_REFERENCE_FILES = [ 'eagle_files/copper_bottom_l4.gbr' ] + @pytest.mark.filterwarnings('ignore:Deprecated.*statement found.*:DeprecationWarning') @pytest.mark.filterwarnings('ignore::SyntaxWarning') @pytest.mark.parametrize('reference', REFERENCE_FILES) def test_round_trip(temp_files, reference): - tmp_gbr, tmp_png = temp_files + tmp_gbr, _tmp_svg, tmp_png = temp_files ref = reference_path(reference) GerberFile.open(ref).save(tmp_gbr) @@ -135,7 +150,7 @@ def test_rotation(temp_files, reference, angle): # gerbv's rendering of this is broken, the hole is missing. return - tmp_gbr, tmp_png = temp_files + tmp_gbr, _tmp_svg, tmp_png = temp_files ref = reference_path(reference) f = GerberFile.open(ref) @@ -156,7 +171,7 @@ def test_rotation_center(temp_files, reference, angle, center): if 'flash_rectangle' in reference and angle in (30, 1024): # gerbv's rendering of this is broken, the hole is missing. return - tmp_gbr, tmp_png = temp_files + tmp_gbr, _tmp_svg, tmp_png = temp_files ref = reference_path(reference) f = GerberFile.open(ref) @@ -165,7 +180,7 @@ def test_rotation_center(temp_files, reference, angle, center): # calculate circle center in SVG coordinates size = (10, 10) # inches - cx, cy = to_gerbv_svg_units(center[0]), to_gerbv_svg_units(10, 'inch')-to_gerbv_svg_units(center[1], 'mm') + 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(ref, tmp_gbr, diff_out=tmp_png, svg_transform=f'rotate({angle} {cx} {cy})', size=size) @@ -178,7 +193,7 @@ def test_rotation_center(temp_files, reference, angle, center): @pytest.mark.parametrize('reference', MIN_REFERENCE_FILES) @pytest.mark.parametrize('offset', TEST_OFFSETS) def test_offset(temp_files, reference, offset): - tmp_gbr, tmp_png = temp_files + tmp_gbr, _tmp_svg, tmp_png = temp_files ref = reference_path(reference) f = GerberFile.open(ref) @@ -201,7 +216,7 @@ def test_combined(temp_files, reference, angle, center, offset): if 'flash_rectangle' in reference and angle in (30, 1024): # gerbv's rendering of this is broken, the hole is missing. return - tmp_gbr, tmp_png = temp_files + tmp_gbr, _tmp_svg, tmp_png = temp_files ref = reference_path(reference) f = GerberFile.open(ref) @@ -210,7 +225,7 @@ def test_combined(temp_files, reference, angle, center, 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(10, 'inch')-to_gerbv_svg_units(center[1], 'mm') + 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(ref, tmp_gbr, diff_out=tmp_png, svg_transform=f'translate({dx} {dy}) rotate({angle} {cx} {cy})', @@ -219,3 +234,54 @@ def test_combined(temp_files, reference, angle, center, offset): assert hist[9] < 100 assert hist[3:].sum() < 1e-3*hist.size +@pytest.mark.filterwarnings('ignore:Deprecated.*statement found.*:DeprecationWarning') +@pytest.mark.filterwarnings('ignore::SyntaxWarning') +@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(temp_files, file_a, file_b, angle, offset): + + # 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': + return + + tmp_gbr, tmp_svg, tmp_png = temp_files + ref_a = reference_path(file_a) + ref_b = reference_path(file_b) + + ax, ay, bx, by = offset + grb_a = GerberFile.open(ref_a) + grb_a.rotate(deg_to_rad(angle)) + grb_a.offset(ax, ay) + + grb_b = GerberFile.open(ref_b) + grb_b.offset(bx, by) + + grb_a.merge(grb_b) + 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=tmp_svg, diff_out=tmp_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 + + -- cgit