[PCB Print] Changed the conversion strategy

- Now we can control the resolution
- Added option for this

Related to #259
This commit is contained in:
Salvador E. Tropea 2022-09-07 08:40:18 -03:00
parent 22d618a425
commit 3684889060
10 changed files with 109 additions and 68 deletions

View File

@ -26,7 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
KiCad bug [11562](https://gitlab.com/kicad/code/kicad/-/issues/11562)
- Internal BoM: KiCad 6 text variables expansion in the fields (#247)
- Compress: Option to store symlinks. (See #265)
- PCB Print: Option to configure the forced edge color. (#281)
- PCB Print:
- Option to configure the forced edge color. (#281)
- Option to control the resolution (DPI). (See #259)
### Fixed
- OAR computation (Report) (#225)
@ -36,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Problems with zones on multiple layers (#226)
- Problems with `hide_excluded: true` and components not in the SCH (#258)
- Text vars generated in the same run didn't show up (#280)
- Low resolution for the solder mask. (See #259)
- SCH Variants on KiCad 6: Problems with missing values in the title block.
- Report: Converted file wasn't stored at `dir` (#238)
- Datasheet download: Time-outs on some servers expecting modern browsers (#240)
@ -53,6 +56,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
kiplot/kibot, import, global, filters, variants, preflight, outputs
- Datasheet download:
- Continue downloading if an SSL certificate error found (#239)
- PCB_Print: PNGs no longer has transparent background. This is because now we
use a PDF as intermediate step.
## [1.2.0] - 2022-06-15

View File

@ -155,14 +155,6 @@ Notes:
[**Colorama**](https://pypi.org/project/Colorama/) [![Python module](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/Python-logo-notext-22x22.png)](https://pypi.org/project/Colorama/) [![PyPi dependency](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/PyPI_logo_simplified-22x22.png)](https://pypi.org/project/Colorama/) [![Debian](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png)](https://packages.debian.org/bullseye/python3-colorama)
- Optional to get color messages in a portable way for general use
[**RSVG tools**](https://gitlab.gnome.org/GNOME/librsvg) v2.40 [![Tool](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png)](https://gitlab.gnome.org/GNOME/librsvg) [![Debian](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png)](https://packages.debian.org/bullseye/librsvg2-bin) ![Auto-download](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/auto_download-22x22.png)
- Optional to:
- Create outputs preview for `navigate_results`
- Create PNG icons for `navigate_results`
- Create PDF, PNG and PS formats for `pcb_print`
- Create EPS format for `pcb_print` (v2.40)
- Create PNG and JPG images for `pcbdraw`
[**Git**](https://git-scm.com/) [![Tool](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png)](https://git-scm.com/) [![Debian](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png)](https://packages.debian.org/bullseye/git) ![Auto-download](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/auto_download-22x22.png)
- Optional to:
- Compare with files in the repo for `diff`
@ -170,16 +162,23 @@ Notes:
- Find commit hash and/or date for `sch_replace`
- Find commit hash and/or date for `set_text_variables`
[**RSVG tools**](https://gitlab.gnome.org/GNOME/librsvg) [![Tool](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png)](https://gitlab.gnome.org/GNOME/librsvg) [![Debian](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png)](https://packages.debian.org/bullseye/librsvg2-bin) ![Auto-download](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/auto_download-22x22.png)
- Optional to:
- Create outputs preview for `navigate_results`
- Create PNG icons for `navigate_results`
- Create PDF, PNG, PS and EPS formats for `pcb_print`
- Create PNG and JPG images for `pcbdraw`
[**ImageMagick**](https://imagemagick.org/) [![Tool](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png)](https://imagemagick.org/) [![Debian](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png)](https://packages.debian.org/bullseye/imagemagick) ![Auto-download](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/auto_download-22x22.png)
- Optional to:
- Create outputs preview for `navigate_results`
- Create monochrome prints for `pcb_print`
- Create monochrome prints and scaled PNG files for `pcb_print`
- Create JPG images for `pcbdraw`
[**Ghostscript**](https://www.ghostscript.com/) [![Tool](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png)](https://www.ghostscript.com/) [![Debian](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png)](https://packages.debian.org/bullseye/ghostscript) ![Auto-download](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/auto_download-22x22.png)
- Optional to:
- Create outputs preview for `navigate_results`
- Create PS files for `pcb_print`
- Create PNG, PS and EPS formats for `pcb_print`
[**Pandoc**](https://pandoc.org/) [![Tool](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png)](https://pandoc.org/) [![Debian](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png)](https://packages.debian.org/bullseye/pandoc)
- Optional to create PDF/ODF/DOCX files for `report`
@ -2226,6 +2225,8 @@ Notes:
- `colored_vias`: [boolean=true] Plot vias in a different color. Like KiCad GUI does.
- `dnf_filter`: [string|list(string)='_none'] Name of the filter to mark components as not fitted.
A short-cut to use for simple cases where a variant is an overkill.
- `dpi`: [number=360] [36,1200] Resolution (Dots Per Inch) for the output file. Most objects are vectors, but thing
like the the solder mask are handled as images by the conversion tools.
- `drill_marks`: [string='full'] [none,small,full] What to use to indicate the drill places, can be none, small or full (for real scale).
- `forced_edge_cuts_color`: [string=''] Color used for the `force_edge_cuts` option.
- `frame_plot_mechanism`: [string='internal'] [gui,internal,plot] Plotting the frame from Python is problematic.
@ -2240,7 +2241,7 @@ Notes:
- `keep_temporal_files`: [boolean=false] Store the temporal page and layer files in the output dir and don't delete them.
- `micro_via_color`: [string=''] Color used for micro `colored_vias`.
- `pad_color`: [string=''] Color used for `colored_pads`.
- `png_width`: [number=1280] Width of the PNG in pixels.
- `png_width`: [number=1280] [0,7680] Width of the PNG in pixels. Use 0 to use as many pixels as the DPI needs for the page size.
- `realistic_solder_mask`: [boolean=true] Try to draw the solder mask as a real solder mask, not the negative used for fabrication.
In order to get a good looking select a color with transparency, i.e. '#14332440'.
PcbDraw must be installed in order to use this option.

View File

@ -1086,6 +1086,9 @@ outputs:
# [string|list(string)='_none'] Name of the filter to mark components as not fitted.
# A short-cut to use for simple cases where a variant is an overkill
dnf_filter: '_none'
# [number=360] [36,1200] Resolution (Dots Per Inch) for the output file. Most objects are vectors, but thing
# like the the solder mask are handled as images by the conversion tools
dpi: 360
# [string='full'] [none,small,full] What to use to indicate the drill places, can be none, small or full (for real scale)
drill_marks: 'full'
# [boolean=false] Add the `Edge.Cuts` to all the pages
@ -1164,7 +1167,7 @@ outputs:
title: ''
# [boolean=true] Include the title-block (worksheet, frame, etc.)
plot_sheet_reference: true
# [number=1280] Width of the PNG in pixels
# [number=1280] [0,7680] Width of the PNG in pixels. Use 0 to use as many pixels as the DPI needs for the page size
png_width: 1280
# [boolean=true] Try to draw the solder mask as a real solder mask, not the negative used for fabrication.
# In order to get a good looking select a color with transparency, i.e. '#14332440'.

View File

@ -8,7 +8,7 @@
from . import PyPDF2
def create_pdf_from_pages(input_files, output_fn):
def create_pdf_from_pages(input_files, output_fn, scale=1.0):
output = PyPDF2.PdfFileWriter()
# Collect all pages
open_files = []
@ -17,6 +17,8 @@ def create_pdf_from_pages(input_files, output_fn):
open_files.append(file)
pdf_reader = PyPDF2.PdfFileReader(file)
page_obj = pdf_reader.getPage(0)
if scale != 1.0:
page_obj.scaleBy(scale)
page_obj.compressContentStreams()
output.addPage(page_obj)
# Write all pages to a file

View File

@ -400,12 +400,12 @@ class BaseOptions(Optionable):
""" Looks for a dependency """
return GS.check_tool_dep(self._parent.type, name, fatal=False)
def expand_filename(self, dir, name, id, ext):
def expand_filename(self, dir, name, id, ext, make_safe=True):
cur_id = self._expand_id
cur_ext = self._expand_ext
self._expand_id = id
self._expand_ext = ext
name = self.expand_filename_both(name, is_sch=self._parent._sch_related)
name = self.expand_filename_both(name, is_sch=self._parent._sch_related, make_safe=make_safe)
res = os.path.abspath(os.path.join(dir, name))
self._expand_id = cur_id
self._expand_ext = cur_ext

View File

@ -7,16 +7,12 @@
"""
Dependencies:
- from: RSVG
role: Create PDF, PNG and PS formats
role: Create PDF, PNG, PS and EPS formats
id: rsvg1
- from: RSVG
role: Create EPS format
version: '2.40'
id: rsvg2
- from: Ghostscript
role: Create PS files
role: Create PNG, PS and EPS formats
- from: ImageMagick
role: Create monochrome prints
role: Create monochrome prints and scaled PNG files
- from: PcbDraw
role: Create realistic solder masks
# The plot_frame_gui() needs KiAuto to print the frame
@ -31,6 +27,13 @@ Dependencies:
role: mandatory
downloader: python
"""
# Direct SVG to EPS conversion is problematic.
# If we use 72 dpi the page size is ok, but some objects (currently the solder mask) have low resolution.
# So we create a PDF and then use GS to create the EPS files.
# - from: RSVG
# role: Create EPS format
# version: '2.40'
# id: rsvg2
import re
import os
import subprocess
@ -247,7 +250,10 @@ class PCB_PrintOptions(VariantOptions):
""" *[PDF,SVG,PNG,EPS,PS] Format for the output file/s.
Note that for PS you need `ghostscript` which isn't part of the default docker images """
self.png_width = 1280
""" Width of the PNG in pixels """
""" [0,7680] Width of the PNG in pixels. Use 0 to use as many pixels as the DPI needs for the page size """
self.dpi = 360
""" [36,1200] Resolution (Dots Per Inch) for the output file. Most objects are vectors, but thing
like the the solder mask are handled as images by the conversion tools """
self.colored_pads = True
""" Plot through-hole in a different color. Like KiCad GUI does """
self.pad_color = ''
@ -860,40 +866,66 @@ class PCB_PrintOptions(VariantOptions):
def svg_to_pdf(self, input_folder, svg_file, pdf_file):
# Note: rsvg-convert uses 90 dpi but KiCad (and the docs I found) says SVG pt is 72 dpi
cmd = [self.rsvg_command, '-d', '72', '-p', '72', '-f', 'pdf', '-o', os.path.join(input_folder, pdf_file),
# We use a 5x scale and then reduce it to maintain the page size
dpi = str(self.dpi)
cmd = [self.rsvg_command, '-d', dpi, '-p', dpi, '-f', 'pdf', '-o', os.path.join(input_folder, pdf_file),
os.path.join(input_folder, svg_file)]
_run_command(cmd)
def svg_to_png(self, input_folder, svg_file, png_file, width):
cmd = [self.rsvg_command, '-w', str(width), '-f', 'png', '-o', os.path.join(input_folder, png_file),
os.path.join(input_folder, svg_file)]
# We can't control the resolution in this way
# def svg_to_png(self, input_folder, svg_file, png_file, width):
# cmd = [self.rsvg_command, '-w', str(width), '-f', 'png', '-o', os.path.join(input_folder, png_file),
# os.path.join(input_folder, svg_file)]
# _run_command(cmd)
# Poor resolution or wrong page size, using a PDF + GS solves it
# def svg_to_eps(self, input_folder, svg_file, eps_file):
# cmd = [self.rsvg_command_eps, '-d', '72', '-p', '72', '-f', 'eps', '-o', os.path.join(input_folder, eps_file),
# os.path.join(input_folder, svg_file)]
# _run_command(cmd)
def run_gs(self, pdf_file, output, device, use_dpi=False):
cmd = [self.gs_command, '-q', '-dNOPAUSE', '-dBATCH', '-P-', '-dSAFER', '-sDEVICE='+device,
'-sOutputFile='+output, '-c', 'save', 'pop', '-f', pdf_file]
if use_dpi:
cmd.insert(1, '-r'+str(self.dpi))
_run_command(cmd)
def svg_to_eps(self, input_folder, svg_file, eps_file):
cmd = [self.rsvg_command_eps, '-d', '72', '-p', '72', '-f', 'eps', '-o', os.path.join(input_folder, eps_file),
os.path.join(input_folder, svg_file)]
_run_command(cmd)
def pdf_to_ps(self, pdf_file, output):
self.run_gs(pdf_file, output, 'ps2write')
def pdf_to_ps(self, ps_file, output):
cmd = [self.gs_command, '-q', '-dNOPAUSE', '-dBATCH', '-P-', '-dSAFER', '-sDEVICE=ps2write', '-sOutputFile='+output,
'-c', 'save', 'pop', '-f', ps_file]
_run_command(cmd)
def pdf_to_eps(self, pdf_file, output):
self.run_gs(pdf_file, output, 'eps2write')
def pdf_to_png(self, pdf_file, output):
self.run_gs(pdf_file, output, 'png16m', use_dpi=True)
if self.png_width:
# Adjust the width
convert_command = self.ensure_tool('ImageMagick')
size = str(self.png_width)+'x'
for n in range(len(self.pages)):
file = output % (n+1)
cmd = [convert_command, file, '-resize', size, file]
_run_command(cmd)
def create_pdf_from_svg_pages(self, input_folder, input_files, output_fn):
""" Convert individual SVG files into individual PDF files using 360 dpi.
Then join the individual PDF files into one PDF file scaled to the right page size. """
svg_files = []
for svg_file in input_files:
pdf_file = svg_file.replace('.svg', '.pdf')
logger.debug('- Creating {} from {}'.format(pdf_file, svg_file))
self.svg_to_pdf(input_folder, svg_file, pdf_file)
svg_files.append(os.path.join(input_folder, pdf_file))
create_pdf_from_pages(svg_files, output_fn)
logger.debug('- Joining {} into {}'.format(svg_files, output_fn))
create_pdf_from_pages(svg_files, output_fn, scale=72.0/self.dpi)
def check_tools(self):
if self.format != 'SVG':
self.rsvg_command = self.ensure_tool('rsvg1')
if self.format == 'PS':
self.gs_command = self.ensure_tool('Ghostscript')
if self.format == 'EPS':
self.rsvg_command_eps = self.ensure_tool('rsvg2')
# if self.format == 'EPS':
# self.rsvg_command_eps = self.ensure_tool('rsvg2')
def generate_output(self, output):
self.check_tools()
@ -990,24 +1022,31 @@ class PCB_PrintOptions(VariantOptions):
assembly_file = GS.pcb_basename+".svg"
logger.debug('- Merging layers to {}'.format(assembly_file))
self.merge_svg(temp_dir, filelist, temp_dir, assembly_file, p)
if self.format in ['PNG', 'EPS']:
id = self._expand_id+('_page_'+page_str)
out_file = self.expand_filename(output_dir, self.output, id, self._expand_ext)
if self.format == 'PNG':
self.svg_to_png(temp_dir, assembly_file, out_file, self.png_width)
else:
self.svg_to_eps(temp_dir, assembly_file, out_file)
pages.append(os.path.join(page_str, assembly_file))
self.restore_title()
# Join all pages in one file
if self.format in ['PDF', 'PS']:
logger.debug('- Creating output file {}'.format(output))
if self.format != 'SVG':
if self.format == 'PDF':
logger.debug('- Creating output file {}'.format(output))
self.create_pdf_from_svg_pages(temp_dir_base, pages, output)
else:
ps_file = os.path.join(temp_dir, GS.pcb_basename+'.ps')
self.create_pdf_from_svg_pages(temp_dir_base, pages, ps_file)
self.pdf_to_ps(ps_file, output)
logger.debug('- Creating output files')
# PS and EPS using Ghostscript
# Create a PDF (but in a temporal place)
pdf_file = os.path.join(temp_dir, GS.pcb_basename+'_joined.pdf')
self.create_pdf_from_svg_pages(temp_dir_base, pages, pdf_file)
if self.format == 'PS':
# Use GS to create one PS
self.pdf_to_ps(pdf_file, output)
else: # EPS and PNG
id = self._expand_id+('_page_%02d')
out_file = self.expand_filename(output_dir, self.output, id, self._expand_ext, make_safe=False)
if self.format == 'EPS':
# Use GS to create one EPS per page
self.pdf_to_eps(pdf_file, out_file)
else:
# Use GS to create one PNG per page and then scale to the wanted width
self.pdf_to_png(pdf_file, out_file)
# Remove the temporal files
if not self.keep_temporal_files:
rmtree(temp_dir_base)

View File

@ -74,7 +74,7 @@ deps = '{\
"version": null\
},\
{\
"desc": "Create PS files",\
"desc": "Create PNG, PS and EPS formats",\
"mandatory": false,\
"output": "pcb_print",\
"version": null\
@ -156,7 +156,7 @@ deps = '{\
"version": null\
},\
{\
"desc": "Create monochrome prints",\
"desc": "Create monochrome prints and scaled PNG files",\
"mandatory": false,\
"output": "pcb_print",\
"version": null\
@ -680,7 +680,7 @@ deps = '{\
"downloader_str": "rsvg",\
"extra_deb": null,\
"help_option": "--version",\
"importance": 5,\
"importance": 4,\
"in_debian": true,\
"is_kicad_plugin": false,\
"is_python": false,\
@ -704,20 +704,11 @@ deps = '{\
"version": null\
},\
{\
"desc": "Create PDF, PNG and PS formats",\
"desc": "Create PDF, PNG, PS and EPS formats",\
"mandatory": false,\
"output": "pcb_print",\
"version": null\
},\
{\
"desc": "Create EPS format",\
"mandatory": false,\
"output": "pcb_print",\
"version": [\
2,\
40\
]\
},\
{\
"desc": "Create PNG and JPG images",\
"mandatory": false,\

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -576,9 +576,9 @@ class TestContext(object):
cmd = ['pdfinfo', file]
logging.debug('Analyzing PDF size: '+usable_cmd(cmd))
res = subprocess.run(cmd, stderr=subprocess.STDOUT, check=True, stdout=subprocess.PIPE).stdout.decode()
m = re.search(r'Page size:\s+(\d+) x (\d+) pts', res)
assert m is not None
return int(m.group(1))/72.0*25.4, int(m.group(2))/72.0*25.4
m = re.search(r'Page size:\s+([\d\.]+) x ([\d\.]+) pts', res)
assert m is not None, res
return float(m.group(1))/72.0*25.4, float(m.group(2))/72.0*25.4
def expect_gerber_flash_at(self, file, res, pos):
"""