Skip to content

Commit

Permalink
[PCB Print][Added][Experimental] A mechanism to paste images
Browse files Browse the repository at this point in the history
- In boxes from groups named "_kibot_image_OUTPUT"
- Only PNGs
- Very raw, WIP
  • Loading branch information
set-soft committed Oct 31, 2024
1 parent 3131f84 commit dcaa564
Show file tree
Hide file tree
Showing 7 changed files with 421 additions and 56 deletions.
7 changes: 4 additions & 3 deletions kibot/bom/html_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,10 @@ def content_table(html, groups, headings, head_names, cfg, link_datasheet, link_


def embed_image(file):
s, w, h = read_png(file)
if s is None:
raise BoMError('Only PNG images are supported for the logo')
try:
s, w, h, _ = read_png(file, logger)
except TypeError as e:
raise BoMError(f'Only PNG images are supported for the logo ({e})')
return int(w), int(h), 'data:image/png;base64,'+b64encode(s).decode('ascii')


Expand Down
34 changes: 5 additions & 29 deletions kibot/kicad/worksheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"""
from base64 import b64decode
import io
from struct import unpack
from pcbnew import wxPoint, wxSize, FromMM, wxPointMM
from ..gs import GS
if not GS.kicad_version_n:
Expand All @@ -34,7 +33,7 @@
from .sexp_helpers import (_check_is_symbol_list, _check_float, _check_integer, _check_symbol_value, _check_str, _check_symbol,
_check_relaxed, _get_points, _check_symbol_str, Color)
from ..svgutils.transform import ImageElement, GroupElement
from ..misc import W_WKSVERSION
from ..misc import W_WKSVERSION, read_png
from .. import log

logger = log.get_logger()
Expand Down Expand Up @@ -425,33 +424,10 @@ def draw(e, p):
p.images.append(e)

def parse_png(e):
s = e.data
offset = 8
ppi = 300
w = h = -1
if s[0:8] != b'\x89PNG\r\n\x1a\n':
raise WksError('Image is not a PNG')
logger.debugl(2, 'Parsing PNG chunks')
while offset < len(s):
size, type = unpack('>L4s', s[offset:offset+8])
logger.debugl(2, f'- Chunk {type} ({size})')
if type == b'IHDR':
w, h = unpack('>LL', s[offset+8:offset+16])
logger.debugl(2, f' - Size {w}x{h}')
elif type == b'pHYs':
dpi_w, dpi_h, units = unpack('>LLB', s[offset+8:offset+17])
if dpi_w != dpi_h:
raise WksError(f'PNG with different resolution for X and Y ({dpi_w} {dpi_h})')
if units != 1:
raise WksError(f'PNG with unknown units ({units})')
ppi = dpi_w/(100/2.54)
logger.debugl(2, f' - PPI {ppi} ({dpi_w} {dpi_h} {units})')
break
elif type == b'IEND':
break
offset += size+12
if w == -1:
raise WksError('Broken PNG, no IHDR chunk')
try:
_, w, h, ppi = read_png(e.data, logger, only_size=False)
except TypeError as e:
raise WksError(str(e))
return w, h, ppi

def add_to_svg(e, svg, p, svg_precision):
Expand Down
43 changes: 36 additions & 7 deletions kibot/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,13 +553,42 @@ def version_str2tuple(ver):
return tuple(map(int, ver.split('.')))


def read_png(file):
with open(file, 'rb') as f:
s = f.read()
if not (s[:8] == b'\x89PNG\r\n\x1a\n' and (s[12:16] == b'IHDR')):
return None, None, None
w, h = unpack('>LL', s[16:24])
return s, w, h
def read_png(file, logger, only_size=True):
if isinstance(file, str):
with open(file, 'rb') as f:
s = f.read()
else:
# The data itself as bytes
s = file
offset = 8
ppi = 300
w = h = -1
if s[0:8] != b'\x89PNG\r\n\x1a\n':
raise TypeError('Image is not a PNG')
logger.debugl(2, 'Parsing PNG chunks')
while offset < len(s):
size, type = unpack('>L4s', s[offset:offset+8])
logger.debugl(2, f'- Chunk {type} ({size})')
if type == b'IHDR':
w, h = unpack('>LL', s[offset+8:offset+16])
logger.debugl(2, f' - Size {w}x{h}')
if only_size:
return s, w, h, ppi
elif type == b'pHYs':
dpi_w, dpi_h, units = unpack('>LLB', s[offset+8:offset+17])
if dpi_w != dpi_h:
raise TypeError(f'PNG with different resolution for X and Y ({dpi_w} {dpi_h})')
if units != 1:
raise TypeError(f'PNG with unknown units ({units})')
ppi = dpi_w/(100/2.54)
logger.debugl(2, f' - PPI {ppi} ({dpi_w} {dpi_h} {units})')
break
elif type == b'IEND':
break
offset += size+12
if w == -1:
raise TypeError('Broken PNG, no IHDR chunk')
return s, w, h, ppi


def force_list(v):
Expand Down
22 changes: 8 additions & 14 deletions kibot/out_navigate_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import pprint
from shutil import copy2
from math import ceil
from struct import unpack
from .bom.kibot_logo import KIBOT_LOGO, KIBOT_LOGO_W, KIBOT_LOGO_H
from .error import KiPlotConfigurationError
from .gs import GS
Expand Down Expand Up @@ -254,15 +253,6 @@ def _run_command(cmd):
return True


def get_png_size(file):
with open(file, 'rb') as f:
s = f.read()
if not (s[:8] == b'\x89PNG\r\n\x1a\n' and (s[12:16] == b'IHDR')):
return 0, 0
w, h = unpack('>LL', s[16:24])
return int(w), int(h)


class Navigate_ResultsOptions(BaseOptions):
def __init__(self):
with document:
Expand Down Expand Up @@ -301,9 +291,10 @@ def config(self, parent):
self.logo = os.path.abspath(self.logo)
if not os.path.isfile(self.logo):
raise KiPlotConfigurationError('Missing logo file `{}`'.format(self.logo))
self._logo_data, self._logo_w, self._logo_h = read_png(self.logo)
if self._logo_data is None:
raise KiPlotConfigurationError('Only PNG images are supported for the logo')
try:
self._logo_data, self._logo_w, self._logo_h, _ = read_png(self.logo, logger)
except TypeError as e:
raise KiPlotConfigurationError(f'Only PNG images are supported for the logo ({e})')
if self.logo == '':
# Internal logo
self._logo_w = int(KIBOT_LOGO_W/2)
Expand Down Expand Up @@ -453,7 +444,10 @@ def get_image_for_file(self, file, out_name, no_icon=False, image=None):
# It was converted, replace the icon by the composited image
img = new_img
# Compute its size
width, height = get_png_size(fimg)
try:
_, width, height, _ = read_png(fimg, logger)
except TypeError:
width = height = 0
# We are using the big size
wide = True
# Now add the image with its file name as caption
Expand Down
105 changes: 102 additions & 3 deletions kibot/out_pcb_print.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
# version: '2.40'
# id: rsvg2
from copy import deepcopy
from dataclasses import dataclass
import datetime
import io
import re
import os
import importlib
Expand All @@ -48,12 +50,14 @@
from .kicad.v5_sch import SchError
from .kicad.pcb import PCB
from .misc import (PDF_PCB_PRINT, W_PDMASKFAIL, W_MISSTOOL, PCBDRAW_ERR, W_PCBDRAW, VIATYPE_THROUGH, VIATYPE_BLIND_BURIED,
VIATYPE_MICROVIA, FONT_HELP_TEXT, W_BUG16418, pretty_list, try_int, W_NOPAGES, W_NOLAYERS, W_NOTHREPE)
VIATYPE_MICROVIA, FONT_HELP_TEXT, W_BUG16418, pretty_list, try_int, W_NOPAGES, W_NOLAYERS, W_NOTHREPE,
RENDERERS, read_png)
from .create_pdf import create_pdf_from_pages
from .macros import macros, document, output_class # noqa: F401
from .drill_marks import DRILL_MARKS_MAP, add_drill_marks
from .layer import Layer, get_priority
from .kiplot import run_command, load_board, get_all_components
from .kiplot import run_command, load_board, get_all_components, look_for_output, get_output_targets, run_output
from .svgutils.transform import ImageElement, GroupElement
from . import __version__
from . import log

Expand All @@ -68,6 +72,14 @@
kicad_worksheet = None # Also needs svgutils


@dataclass
class ImageGroup:
name: str
layer: int
bbox: tuple
items: list


def pcbdraw_warnings(tag, msg):
logger.warning('{}({}) {}'.format(W_PCBDRAW, tag, msg))

Expand Down Expand Up @@ -662,7 +674,7 @@ def plot_frame_ki8_external(self, dir_name, p, page, pages, color):
# https://gitlab.com/kicad/code/kicad/-/issues/18959
logger.debugl(1, ' - Fixing images')
# Do a manual draw, just to collect any image
ws.draw(GS.board, GS.board.GetLayerID('Rescue'), page, self.pcb.paper_w, self.pcb.paper_h, tb_vars)
ws.draw(GS.board, GS.board.GetLayerID(GS.work_layer), page, self.pcb.paper_w, self.pcb.paper_h, tb_vars)
ws.undraw(GS.board)
# We need to plot the images in a separated pass
self.last_worksheet = ws
Expand Down Expand Up @@ -926,6 +938,88 @@ def search_text(self, svg, texts):
# Process all text inside
self.search_text_for_g(e, texts)

def move_kibot_image_groups(self):
""" Look for KiBot image groups (_kibot_image_*)
Move them to the Rescue layer
Memorize them to restore and analysis """
self._image_groups = []
tmp_layer = GS.board.GetLayerID(GS.work_layer)
for g in GS.board.Groups():
name = g.GetName()
if not name.startswith('_kibot_image_'):
continue
x1, y1, x2, y2 = GS.compute_group_boundary(g)
moved = []
layer = None
for item in g.GetItems():
if layer is None:
layer = item.GetLayer()
moved.append((item, item.GetLayer()))
item.SetLayer(tmp_layer)
self._image_groups.append(ImageGroup(name, layer, (x1, y1, x2, y2), moved))

def restore_kibot_image_groups(self):
""" Move the KiBot image groups (_kibot_image_*) to their original layers """
for g in self._image_groups:
for item in g.items:
item[0].SetLayer(item[1])
self._image_groups = []

def add_output_images(self, svg, page):
""" Look for groups named _kibot_image_OUTPUT and paste images from the referred OUTPUTs """
# Check which layers we printed
layers = {la._id for la in page._layers}
# Look for groups
logger.debug('Looking for image groups in the PCB')
for g in self._image_groups:
name = g.name
if not name.startswith('_kibot_image_'):
continue
logger.debugl(2, f'- Found {name}')
# Check if this group is for a layer we printed
if g.layer not in layers:
logger.debug('- {name} not in printed layers')
continue
# Look for the image from the output
output_name = name[13:]
output_obj = look_for_output(output_name, '`include image`', self._parent, RENDERERS)
targets, _, _ = get_output_targets(output_name, self._parent)
targets = [fn for fn in targets if fn.endswith('.png')]
if not targets:
raise KiPlotConfigurationError("PCB group `{name}` uses `{output_name}` which doesn't generate any PNG")
fname = targets[0]
logger.debugl(2, f'- Related image: {fname}')
if not os.path.exists(fname):
# The target doesn't exist
if not output_obj._done:
# The output wasn't created in this run, try running it
logger.debug('- Not yet generated, tying to generate it')
run_output(output_obj)
if not os.path.exists(fname):
raise KiPlotConfigurationError("Failed to generate `{fname}` for PCB group `{name}`")
# Add the image to the SVG
try:
s, w, h, dpi = read_png(fname, logger, only_size=False)
except TypeError as e:
raise KiPlotConfigurationError(f'Error reading {fname} size: {e} for PCB group `{name}`')
logger.debugl(2, f'- PNG: {w}x{h} {dpi} PPIs')
x1, y1, x2, y2 = g.bbox
logger.debugl(2, f'- Box: {x1},{y1} {x2},{y2} IUs')
# Convert pixels to mm and then to KiCad units
# w = GS.from_mm(w/dpi*25.4)
# h = GS.from_mm(h/dpi*25.4)
# logger.error(f'{w}x{h} IUs')
scale = GS.iu_to_svg(1.0, self.svg_precision) # This is the scale to convert IUs to SVG units
logger.debugl(2, f'- Scale {scale}')
# Put the image at the box coordinates with its size
img = ImageElement(io.BytesIO(s), x2-x1, y2-y1)
img.moveto(x1, y1)
img.scale(scale)
# Put the image in a group
g = GroupElement([img])
# Add the group to the SVG
svg.append(g)

def merge_svg(self, input_folder, input_files, output_folder, output_file, p):
""" Merge all layers into one page """
first = True
Expand All @@ -950,6 +1044,7 @@ def merge_svg(self, input_folder, input_files, output_folder, output_file, p):
first = False
self.process_background(svg_out, width, height)
self.add_frame_images(svg_out, p.monochrome)
self.add_output_images(svg_out, p)
else:
root = new_layer.getroot()
# Adjust the coordinates of this section to the main width
Expand Down Expand Up @@ -1315,6 +1410,8 @@ def generate_output(self, output):
# Make visible only the layers we need
# This is very important when scaling, otherwise the results are controlled by the .kicad_prl (See #407)
self.set_visible(edge_id)
# Move KiBot image groups away
self.move_kibot_image_groups()
# Generate the output, page by page
pages = []
for n, p in enumerate(self._pages):
Expand Down Expand Up @@ -1450,6 +1547,8 @@ def generate_output(self, output):
# Use GS to create one PNG per page and then scale to the wanted width
self.pdf_to_png(pdf_file, out_file)
self.rename_pages(output_dir)
# Restore KiBot image groups away
self.restore_kibot_image_groups()
# Remove the temporal files
if not self.keep_temporal_files:
rmtree(temp_dir_base)
Expand Down
Loading

0 comments on commit dcaa564

Please sign in to comment.