334 lines
12 KiB
Python
Executable file
334 lines
12 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
#
|
|
# Copyright (C) 2012 Alvin Penner, penner@vaxxine.com
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
"""
|
|
This extension will generate vector graphics printout, specifically for Windows GDI32.
|
|
|
|
This is a modified version of the file dxf_outlines.py by Aaron Spike, aaron@ekips.org
|
|
It will write only to the default printer.
|
|
The printing preferences dialog will be called.
|
|
In order to ensure a pure vector output, use a linewidth < 1 printer pixel
|
|
|
|
- see http://www.lessanvaezi.com/changing-printer-settings-using-the-windows-api/
|
|
- get GdiPrintSample.zip at http://archive.msdn.microsoft.com/WindowsPrintSample
|
|
|
|
"""
|
|
|
|
import sys
|
|
import ctypes
|
|
import struct
|
|
|
|
import inkex
|
|
from inkex import (
|
|
PathElement,
|
|
ShapeElement,
|
|
Rectangle,
|
|
Group,
|
|
Use,
|
|
Transform,
|
|
Line,
|
|
Circle,
|
|
Ellipse,
|
|
)
|
|
from inkex.paths import Path
|
|
|
|
if sys.platform.startswith("win"):
|
|
# The wintypes module raises a ValueError when imported on non-Windows systems
|
|
# for certain Python versions.
|
|
from ctypes import wintypes
|
|
|
|
myspool = ctypes.WinDLL("winspool.drv")
|
|
mygdi = ctypes.WinDLL("gdi32.dll")
|
|
|
|
class LOGBRUSH(ctypes.Structure):
|
|
_fields_ = [
|
|
("lbStyle", wintypes.UINT),
|
|
("lbColor", wintypes.COLORREF),
|
|
("lbHatch", wintypes.PULONG),
|
|
]
|
|
|
|
DM_IN_PROMPT = 4 # call printer property sheet
|
|
DM_OUT_BUFFER = 2 # write to DEVMODE structure
|
|
|
|
LOGPIXELSX = 88
|
|
LOGPIXELSY = 90
|
|
|
|
class DOCINFO(ctypes.Structure):
|
|
_fields_ = [
|
|
("cbSize", ctypes.c_int),
|
|
("lpszDocName", wintypes.LPCSTR),
|
|
("lpszOutput", wintypes.LPCSTR),
|
|
("lpszDatatype", wintypes.LPCSTR),
|
|
("fwType", wintypes.DWORD),
|
|
]
|
|
|
|
StartDocA = mygdi.StartDocA
|
|
StartDocA.argtypes = [wintypes.HDC, ctypes.POINTER(DOCINFO)]
|
|
StartDocA.restype = ctypes.c_int
|
|
|
|
EndDoc = mygdi.EndDoc
|
|
EndDoc.argtypes = [wintypes.HDC]
|
|
EndDoc.restype = ctypes.c_int
|
|
|
|
CreateDCA = mygdi.CreateDCA
|
|
CreateDCA.argtypes = [
|
|
wintypes.LPCSTR,
|
|
wintypes.LPCSTR,
|
|
wintypes.LPCSTR,
|
|
wintypes.LPCVOID,
|
|
]
|
|
CreateDCA.restype = wintypes.HDC
|
|
|
|
GetDefaultPrinterA = myspool.GetDefaultPrinterA
|
|
GetDefaultPrinterA.argtypes = [wintypes.LPSTR, wintypes.LPDWORD]
|
|
GetDefaultPrinterA.restype = wintypes.BOOL
|
|
|
|
OpenPrinterA = myspool.OpenPrinterA
|
|
OpenPrinterA.argtypes = [wintypes.LPSTR, wintypes.LPHANDLE, wintypes.LPCVOID]
|
|
OpenPrinterA.restype = wintypes.BOOL
|
|
|
|
ClosePrinter = myspool.ClosePrinter
|
|
ClosePrinter.argtypes = [wintypes.HANDLE]
|
|
ClosePrinter.restype = wintypes.BOOL
|
|
|
|
DocumentPropertiesA = myspool.DocumentPropertiesA
|
|
DocumentPropertiesA.argtypes = [
|
|
wintypes.HWND,
|
|
wintypes.HANDLE,
|
|
wintypes.LPSTR,
|
|
wintypes.LPVOID,
|
|
wintypes.LPVOID,
|
|
wintypes.DWORD,
|
|
]
|
|
DocumentPropertiesA.restype = wintypes.LONG
|
|
|
|
SelectObject = mygdi.SelectObject
|
|
SelectObject.argtypes = [wintypes.HDC, wintypes.HGDIOBJ]
|
|
SelectObject.restype = wintypes.HGDIOBJ
|
|
|
|
MoveToEx = mygdi.MoveToEx
|
|
MoveToEx.argtypes = [wintypes.HDC, ctypes.c_int, ctypes.c_int, wintypes.LPCVOID]
|
|
MoveToEx.restype = wintypes.BOOL
|
|
|
|
CreateBrushIndirect = mygdi.CreateBrushIndirect
|
|
CreateBrushIndirect.argtypes = [ctypes.POINTER(LOGBRUSH)]
|
|
CreateBrushIndirect.restype = wintypes.HBRUSH
|
|
|
|
CreatePen = mygdi.CreatePen
|
|
CreatePen.argtypes = [ctypes.c_int, ctypes.c_int, wintypes.COLORREF]
|
|
CreatePen.restype = wintypes.HPEN
|
|
|
|
BeginPath = mygdi.BeginPath
|
|
BeginPath.argtypes = [wintypes.HDC]
|
|
BeginPath.restype = wintypes.BOOL
|
|
|
|
EndPath = mygdi.EndPath
|
|
EndPath.argtypes = [wintypes.HDC]
|
|
EndPath.restype = wintypes.BOOL
|
|
|
|
FillPath = mygdi.FillPath
|
|
FillPath.argtypes = [wintypes.HDC]
|
|
FillPath.restype = wintypes.BOOL
|
|
|
|
PolyBezierTo = mygdi.PolyBezierTo
|
|
PolyBezierTo.argtypes = [wintypes.HDC, wintypes.LPCVOID, wintypes.DWORD]
|
|
|
|
GetDeviceCaps = mygdi.GetDeviceCaps
|
|
GetDeviceCaps.argtypes = [wintypes.HDC, ctypes.c_int]
|
|
GetDeviceCaps.restype = ctypes.c_int
|
|
|
|
|
|
class PrintWin32Vector(inkex.EffectExtension):
|
|
def __init__(self):
|
|
super(PrintWin32Vector, self).__init__()
|
|
|
|
def process_shape(self, node, mat):
|
|
"""Process shape"""
|
|
pen_color = 0 # Stroke color
|
|
fill_color = None # Fill color
|
|
pen_width = 1 # Pen width in printer pixels
|
|
|
|
# Win32 API expect colors in 0x00bbggrr format.
|
|
def to_bgr(color):
|
|
color = color.to_rgb()
|
|
return (color[2] << 16) + (color[1] << 8) + color[0]
|
|
|
|
if not isinstance(node, (PathElement, Rectangle, Line, Circle, Ellipse)):
|
|
return
|
|
|
|
# Very NB : If the pen width is greater than 1 then the output will Not be a vector output !
|
|
style = node.style
|
|
if style:
|
|
attr = style("stroke")
|
|
if attr is not None and isinstance(attr, inkex.Color):
|
|
pen_color = to_bgr(attr)
|
|
attr = style("fill")
|
|
if attr is not None and isinstance(attr, inkex.Color):
|
|
fill_color = to_bgr(attr)
|
|
if "stroke-width" in style:
|
|
pen_width = self.svg.to_dimensionless(style("stroke-width").strip())
|
|
pen_width = int(pen_width * self.scale_xy)
|
|
|
|
path = node.path.to_superpath().transform(mat @ node.transform)
|
|
|
|
hPen = CreatePen(0, pen_width, pen_color)
|
|
SelectObject(self.hDC, hPen)
|
|
self.emit_path(path)
|
|
if fill_color is not None:
|
|
brush = LOGBRUSH(0, fill_color, None)
|
|
hBrush = CreateBrushIndirect(ctypes.byref(brush))
|
|
SelectObject(self.hDC, hBrush)
|
|
BeginPath(self.hDC)
|
|
self.emit_path(path)
|
|
EndPath(self.hDC)
|
|
FillPath(self.hDC)
|
|
return
|
|
|
|
def emit_path(self, p):
|
|
for sub in p:
|
|
MoveToEx(self.hDC, int(sub[0][1][0]), int(sub[0][1][1]), None)
|
|
POINTS = ctypes.c_long * (6 * (len(sub) - 1))
|
|
points = POINTS()
|
|
for i in range(len(sub) - 1):
|
|
points[6 * i] = int(sub[i][2][0])
|
|
points[6 * i + 1] = int(sub[i][2][1])
|
|
points[6 * i + 2] = int(sub[i + 1][0][0])
|
|
points[6 * i + 3] = int(sub[i + 1][0][1])
|
|
points[6 * i + 4] = int(sub[i + 1][1][0])
|
|
points[6 * i + 5] = int(sub[i + 1][1][1])
|
|
PolyBezierTo(self.hDC, ctypes.addressof(points), 3 * (len(sub) - 1))
|
|
|
|
def process_clone(self, node):
|
|
trans = node.get("transform")
|
|
x = node.get("x")
|
|
y = node.get("y")
|
|
mat = Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
|
|
if trans:
|
|
mat @= Transform(trans)
|
|
if x:
|
|
mat @= Transform([[1.0, 0.0, float(x)], [0.0, 1.0, 0.0]])
|
|
if y:
|
|
mat @= Transform([[1.0, 0.0, 0.0], [0.0, 1.0, float(y)]])
|
|
# push transform
|
|
if trans or x or y:
|
|
self.groupmat.append(Transform(self.groupmat[-1]) @ mat)
|
|
# get referenced node
|
|
refnode = node.href
|
|
if refnode is not None:
|
|
if isinstance(refnode, inkex.Group):
|
|
self.process_group(refnode)
|
|
elif refnode.tag == "svg:use":
|
|
self.process_clone(refnode)
|
|
else:
|
|
self.process_shape(refnode, self.groupmat[-1])
|
|
# pop transform
|
|
if trans or x or y:
|
|
self.groupmat.pop()
|
|
|
|
def process_group(self, group):
|
|
trans = group.get("transform")
|
|
if trans:
|
|
self.groupmat.append(self.groupmat[-1] @ Transform(trans))
|
|
for node in group:
|
|
if not isinstance(node, ShapeElement) or not node.is_visible():
|
|
continue
|
|
if isinstance(node, Group):
|
|
self.process_group(node)
|
|
elif isinstance(node, Use):
|
|
self.process_clone(node)
|
|
else:
|
|
self.process_shape(node, self.groupmat[-1])
|
|
if trans:
|
|
self.groupmat.pop()
|
|
|
|
def doc_name(self):
|
|
docname = self.svg.xpath("@sodipodi:docname")
|
|
if not docname:
|
|
docname = ["New document 1"]
|
|
return ctypes.create_string_buffer(
|
|
("Inkscape " + docname[0].split("\\")[-1]).encode("ascii", "replace")
|
|
)
|
|
|
|
def effect(self):
|
|
pcchBuffer = wintypes.DWORD()
|
|
GetDefaultPrinterA(None, ctypes.byref(pcchBuffer)) # get length of printer name
|
|
pname = ctypes.create_string_buffer(pcchBuffer.value)
|
|
GetDefaultPrinterA(pname, ctypes.byref(pcchBuffer)) # get printer name
|
|
hPrinter = wintypes.HANDLE()
|
|
if OpenPrinterA(pname.value, ctypes.byref(hPrinter), None) == 0:
|
|
return inkex.errormsg(_("Failed to open default printer"))
|
|
|
|
# get printer properties dialog
|
|
pcchBuffer = DocumentPropertiesA(0, hPrinter, pname, None, None, 0)
|
|
pDevMode = ctypes.create_string_buffer(
|
|
pcchBuffer + 100
|
|
) # allocate extra just in case
|
|
pcchBuffer = DocumentPropertiesA(
|
|
0,
|
|
hPrinter,
|
|
pname,
|
|
ctypes.byref(pDevMode),
|
|
None,
|
|
DM_IN_PROMPT + DM_OUT_BUFFER,
|
|
)
|
|
ClosePrinter(hPrinter)
|
|
if pcchBuffer != 1: # user clicked Cancel
|
|
exit()
|
|
|
|
# initialize print document
|
|
lpszDocName = self.doc_name()
|
|
docInfo = DOCINFO(
|
|
ctypes.sizeof(DOCINFO), ctypes.addressof(lpszDocName), 0, 0, 0
|
|
)
|
|
self.hDC = CreateDCA(None, pname, None, ctypes.byref(pDevMode))
|
|
if StartDocA(self.hDC, ctypes.byref(docInfo)) < 0:
|
|
exit() # user clicked Cancel
|
|
|
|
# Take the DPI value from the DC instead of extracting it from pDevMode, this
|
|
# method works when print drivers return device-independent quality values (e.g.
|
|
# DMRES_MEDIUM).
|
|
dpi_x = GetDeviceCaps(self.hDC, LOGPIXELSX)
|
|
dpi_y = GetDeviceCaps(self.hDC, LOGPIXELSY)
|
|
# Convert from dots-per-inch (DPI) to user units.
|
|
self.scale_x = dpi_x / self.svg.viewport_to_unit("1in")
|
|
self.scale_y = dpi_y / self.svg.viewport_to_unit("1in")
|
|
|
|
doc = self.document.getroot()
|
|
# Process viewBox height attribute to correct page scaling.
|
|
viewBox = self.svg.get_viewbox()
|
|
if viewBox and viewBox[2] and viewBox[3]:
|
|
doc_width = self.svg.viewbox_width
|
|
doc_height = self.svg.viewbox_height
|
|
self.scale_x *= doc_width / self.svg.viewport_to_unit(
|
|
self.svg.add_unit(viewBox[2])
|
|
)
|
|
self.scale_y *= doc_height / self.svg.viewport_to_unit(
|
|
self.svg.add_unit(viewBox[3])
|
|
)
|
|
|
|
self.scale_xy = (self.scale_x + self.scale_y) / 2
|
|
self.groupmat = [
|
|
Transform([[self.scale_x, 0.0, 0.0], [0.0, self.scale_y, 0.0]])
|
|
]
|
|
self.process_group(doc)
|
|
EndDoc(self.hDC)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
PrintWin32Vector().run()
|