Compare commits

...

19 Commits

Author SHA1 Message Date
lorenz.stechauner cf11791dad winziprint.py: Add feature to abort jobs 2026-01-16 00:17:13 +01:00
lorenz.stechauner 1cf775beec README: Fix small issues 2025-02-21 16:22:11 +01:00
lorenz.stechauner 9cab6f0a00 winziprint.py: Fix 'sequence index out of range' 2024-03-09 22:29:23 +01:00
lorenz.stechauner 3e0d004e82 winziprint.py: Fix 'list assignment index out of range' error 2 2024-03-09 22:06:08 +01:00
lorenz.stechauner 1a425915d7 winziprint.py: Fix 'list assignment index out of range' error 2024-03-09 21:16:00 +01:00
lorenz.stechauner ad12481e50 winziprint.py: Flush stdout in tcp_server() 2024-03-05 17:14:31 +01:00
lorenz.stechauner 640e025c2d winziprint.py: Rename function daemon to tcp_server
To avoid Windows Defender exterminating the .exe file
2024-03-05 17:07:22 +01:00
lorenz.stechauner f2f3e10e2a winziprint.py: Extract daemon() function from main() 2024-03-05 12:33:30 +01:00
lorenz.stechauner 75689983df winziprint.py: Fix bug where letterhead from tmp_file is only first page 2024-03-02 18:35:20 +01:00
lorenz.stechauner 7945e0ae8a winziprint.py: Fix list index error 2024-03-02 18:24:29 +01:00
lorenz.stechauner 8f0588e8f3 winziprint.py: Fix letterhead positioning 2024-03-01 11:57:51 +01:00
lorenz.stechauner 4d5020f2b9 winziprint.py: Fix list index out of range error 2024-03-01 01:42:09 +01:00
lorenz.stechauner 0d014a4b34 winziprint.py: Add option to add letterhead 2024-03-01 00:02:13 +01:00
lorenz.stechauner 0ec9092df5 README: Fix small issue 2024-02-29 23:03:27 +01:00
lorenz.stechauner 40c1b68d51 winziprint.py: Rename --double-sided to --double-paged 2024-02-29 23:01:15 +01:00
lorenz.stechauner 3abb764ca3 winziprint.py: Make steps variable thread safe 2024-01-14 20:00:26 +01:00
lorenz.stechauner 81848ac767 winziprint.py: Extract convert_part() from convert() 2024-01-14 19:48:02 +01:00
lorenz.stechauner 5b3b96fb35 README: Add Usage 2024-01-14 12:14:10 +01:00
lorenz.stechauner cd6b30cf80 [#1] winziprint.py: Add server mode 2024-01-14 12:13:53 +01:00
4 changed files with 208 additions and 52 deletions
+1
View File
@@ -1,3 +1,4 @@
.idea
__pycache__
*.exe
*.spec
+3 -1
View File
@@ -1,6 +1,8 @@
pyinstaller --noconfirm ^
pyinstaller ^
--noconfirm ^
--onefile ^
--console ^
--icon "NONE" ^
--name "WinziPrint" ^
--clean ^
src/winziprint.py
+40
View File
@@ -3,3 +3,43 @@
A standalone [WeasyPrint](https://weasyprint.org/) wrapper for Windows.
# Usage
Synopsis:
```
winziprint [-h] [-v] [-d DIR] [ -D | [-p] [-2] [-e ENCODING] [ - | INPUT [INPUT...] OUTPUT ] ]
```
Command line options:
* `-h`, `--help` - show help message and exit
* `-V`, `--version` - show version and exit
* `-D`, `--daemon` - run as a daemon by listening on a TCP socket (port 30983)
* `-d`, `--directory` - set the working directory
* `-e`, `--encoding` - encoding of the input files
* `-2`, `--double-paged` - pad documents to an even number of pages
* `-p`, `--progress` - show progress updates
Command line arguments:
* `-` - use stdin for retrieving input and output file names (semi-colon-seperated)
* `INPUT` - name of a html input file
* `OUTPUT` - name of a pdf output file
## Normal mode
In normal mode all options, input and output files are passed by command line arguments.
## Batch mode
To activate batch mode `-` has to be passed as only input or output file.
In batch mode all options may be passed as command line arguments. These are used as defaults.
## Server mode
To activate server mode the `-D` flag has to be set.
Keep in mind, that not all command line options may be present.
The protocol is the same as in server mode.
+164 -51
View File
@@ -1,73 +1,126 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Union
from typing import TextIO, Callable
import os
import sys
import time
import traceback
import gc
import socketserver
import io
import signal
import math
import threading
import weasyprint
import pypdf
VERSION = __version__ = '0.1.0'
VERSION = __version__ = '0.3.0'
SOCKET_ADDRESS = ('127.0.0.1', 30983)
BATCH_SIZE = 10
RUNNING: dict[int, bool] = {}
def convert_part(output_file: str, batch: list[str], step_cb: Callable, encoding: str = None) -> list[int]:
documents = []
for n, file_name in enumerate(batch):
html = weasyprint.HTML(filename=file_name, encoding=encoding)
doc = html.render()
documents.append(doc)
del html
step_cb()
all_pages = [p for doc in documents for p in doc.pages]
documents[0].copy(all_pages).write_pdf(output_file)
tmp_page_nums = [len(doc.pages) for doc in documents]
del documents
del all_pages
gc.collect()
step_cb()
return tmp_page_nums
def convert(input_files: list[str],
output_files: str,
output_file: str,
encoding: str = None,
padding: bool = False,
progress: bool = False) -> list[int]:
progress: bool = False,
out: TextIO = sys.stdout) -> list[int]:
# it takes roughly 100ms to generate one document
tmp_page_nums = []
tmp_file_names = []
page_nums = []
html_files = [file.lstrip('!') for file in input_files if not file.endswith('.pdf')]
steps = len(html_files) + len(html_files) // BATCH_SIZE + 1
html_files = [file.lstrip('!#') for file in input_files if not file.endswith('.pdf')]
total_steps = len(html_files) + math.ceil(len(html_files) / BATCH_SIZE) + 1
steps = [0]
def next_step() -> None:
if not RUNNING[threading.get_ident()]:
raise InterruptedError('aborted')
steps[0] += 1
if progress:
print(f'progress: {steps[0]}/{total_steps}', file=out, flush=True)
try:
tmp_page_nums = []
for i in range(0, len(html_files), BATCH_SIZE):
tmp_file = f'{output_file}.{i:04}.part'
tmp_file_names.append(tmp_file)
batch = html_files[i:i + BATCH_SIZE]
documents = []
for n, file_name in enumerate(batch):
html = weasyprint.HTML(filename=file_name, encoding=encoding)
doc = html.render()
documents.append(doc)
del html
if progress:
print(f'progress: {i + n + i // BATCH_SIZE + 1}/{steps}', flush=True)
all_pages = [p for doc in documents for p in doc.pages]
tmp_file_name = f'{output_files}.{i:04}.part'
documents[0].copy(all_pages).write_pdf(tmp_file_name)
tmp_file_names.append(tmp_file_name)
tmp_page_nums += [len(doc.pages) for doc in documents]
del documents
del all_pages
gc.collect()
if progress and i < BATCH_SIZE:
print(f'progress: {i + BATCH_SIZE + i // BATCH_SIZE + 1}/{steps}', flush=True)
tmp_page_nums += convert_part(tmp_file, batch, next_step, encoding=encoding)
letterhead = None
merger = pypdf.PdfWriter()
i = 0
for n, file_name in enumerate(input_files):
p0 = len(merger.pages)
if letterhead and file_name.startswith('#'):
if len(merger.pages) <= letterhead[1]:
merger.add_page(letterhead[0])
merger.add_blank_page()
else:
merger.insert_page(letterhead[0], index=letterhead[1])
merger.insert_blank_page(index=letterhead[1] + 1)
page_nums[letterhead[2]] = 1
letterhead = None
if file_name.endswith('.pdf'):
merger.append(file_name.lstrip('!'))
if padding and file_name.startswith('#'):
r = pypdf.PdfReader(file_name.lstrip('!#'))
letterhead = (r.pages[0], p0, n)
del r
else:
merger.append(file_name.lstrip('!#'))
else:
batch_page_nums = tmp_page_nums[i // BATCH_SIZE * BATCH_SIZE:(i // BATCH_SIZE + 1) * BATCH_SIZE]
page_start = sum(batch_page_nums[:i % BATCH_SIZE])
merger.append(tmp_file_names[n // BATCH_SIZE], pages=(page_start, page_start + tmp_page_nums[i]))
tmp_file_name = tmp_file_names[i // BATCH_SIZE]
if padding and file_name.startswith('#'):
r = pypdf.PdfReader(tmp_file_name)
letterhead = (r.pages[page_start], p0, n)
del r
else:
merger.append(tmp_file_name, pages=(page_start, page_start + tmp_page_nums[i]))
i += 1
p1 = len(merger.pages)
page_nums.append(p1 - p0)
if padding and file_name[0] != '!' and len(merger.pages) % 2 != 0:
if padding and file_name[0] not in ('!', '#') and len(merger.pages) % 2 != 0:
if letterhead:
merger.add_page(letterhead[0])
letterhead = None
else:
merger.add_blank_page()
if letterhead:
if len(merger.pages) <= letterhead[1]:
merger.add_page(letterhead[0])
merger.add_blank_page()
merger.write(output_files)
else:
merger.insert_page(letterhead[0], index=letterhead[1])
merger.insert_blank_page(index=letterhead[1] + 1)
page_nums[letterhead[2]] = 1
merger.write(output_file)
merger.close()
del merger
finally:
@@ -75,52 +128,84 @@ def convert(input_files: list[str],
if os.path.isfile(pdf):
os.remove(pdf)
if progress:
print(f'progress: {steps}/{steps}', flush=True)
next_step()
return page_nums
def _wrapper_convert(args: list[str], encoding: str = None, padding: bool = False, progress: bool = False) -> None:
def _wrapper_convert(args: list[str],
encoding: str = None,
padding: bool = False,
progress: bool = False,
out: TextIO = sys.stdout) -> None:
try:
if len(args) < 2:
print(f'error: Too few arguments', flush=True)
print(f'error: Too few arguments', file=out, flush=True)
return
inputs = args[:-1]
output = args[-1]
if inputs[0] == '-2':
inputs.pop(0)
padding = True
while len(inputs) > 0:
if inputs[0] == '-2':
inputs.pop(0)
padding = True
elif inputs[0].startswith('-e'):
encoding = inputs.pop(0)[2:].strip()
elif inputs[0].startswith('-p'):
inputs.pop(0)
progress = True
else:
break
if len(inputs) == 0:
print(f'error: Too few arguments', file=out, flush=True)
return
t0 = time.process_time()
pages = convert(inputs, output, encoding=encoding, padding=padding, progress=progress)
pages = convert(inputs, output, encoding=encoding, padding=padding, progress=progress, out=out)
total = sum(p + 1 if padding and p % 2 != 0 else p for p in pages)
t1 = time.process_time()
print(f'success: '
f'{len(args) - 1} documents, '
f'{len(inputs)} documents, '
f'{total} pages ({", ".join(str(p) for p in pages)}), '
f'{t1 - t0:.1f} sec',
flush=True)
file=out, flush=True)
except Exception as e:
msg = str(e).replace('\n', ' ')
print(f'error: {msg}', flush=True)
traceback.print_exception(e)
print(f'error: {msg}', file=out, flush=True)
traceback.print_exception(e, file=sys.stderr)
finally:
gc.collect()
def tcp_server() -> None:
# a tcp server is used due to the lack of unix sockets on Windows
with socketserver.ThreadingTCPServer(SOCKET_ADDRESS, ConnectionHandler) as server:
def exit_gracefully(_signum: int, _frame) -> None:
raise KeyboardInterrupt()
signal.signal(signal.SIGINT, exit_gracefully)
signal.signal(signal.SIGTERM, exit_gracefully)
print('Running as daemon', flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
print('', file=sys.stderr, flush=True)
print('Shutting down', flush=True)
sys.exit(0)
def usage(error: bool = False) -> None:
print(f'usage: {sys.argv[0]} [-h] [-v] [-p] [-2] [-d DIR] [-e ENCODING] [ - | INPUT [INPUT...] OUTPUT ]\n\n'
print(f'usage: {sys.argv[0]} [-h] [-v] [-d DIR] [ -D | [-p] [-2] [-e ENCODING] [ - | INPUT [INPUT...] OUTPUT ] ]\n'
'\n'
'options:\n'
' -h, --help show this help message and exit\n'
' -v, --version show version and exit\n'
' -V, --version show version and exit\n'
' -D, --daemon run as a daemon and expose a named socket\n'
' -d, --directory set the working directory\n'
' -e, --encoding encoding of the input files\n'
' -2, --double-sided pad documents to an even number of pages\n'
' -2, --double-paged pad documents to an even number of pages\n'
' -p, --progress show progress updates\n'
'\n'
' - use stdin for retrieving input and output file names (semi-colon-seperated)\n'
' INPUT name of an html input file\n'
' OUTPUT name of an pdf output file',
' INPUT name of a html input file\n'
' OUTPUT name of a pdf output file',
file=sys.stderr if error else sys.stdout)
sys.exit(1 if error else 0)
@@ -132,7 +217,7 @@ def version() -> None:
sys.exit(0)
def _get_arg(args: list[str], n1: str, n2: str = None, flag: bool = False) -> Union[None, str, bool]:
def _get_arg(args: list[str], n1: str, n2: str = None, flag: bool = False) -> None | str | bool:
v = None
for n in [n1] + (n2 and [n2] or []):
if flag:
@@ -150,27 +235,55 @@ def _get_arg(args: list[str], n1: str, n2: str = None, flag: bool = False) -> Un
return v if not flag else v or False
class ConnectionHandler(socketserver.StreamRequestHandler):
def handle(self):
try:
out = io.TextIOWrapper(self.wfile, encoding='utf-8')
RUNNING[threading.get_ident()] = True
print(f'id: {threading.get_ident()}', file=out, flush=True)
for line in io.TextIOWrapper(self.rfile, encoding='utf-8'):
if not RUNNING[threading.get_ident()]:
print(f'error: aborted', file=out, flush=True)
break
parts = line.strip().split(';')
if len(parts) == 2 and parts[0] == 'cancel':
thread_id = int(parts[1].strip())
RUNNING[thread_id] = False
continue
_wrapper_convert(parts, out=out)
except (ValueError, ConnectionError):
pass # socket closed by client
finally:
del RUNNING[threading.get_ident()]
def main() -> None:
args = sys.argv[1:]
if len(args) == 0 or '-h' in args or '--help' in args:
usage()
elif '-v' in args or '--version' in args:
elif '-V' in args or '--version' in args:
version()
working_dir = _get_arg(args, '-d', '--directory')
if working_dir:
os.chdir(working_dir)
if '-D' in args:
if len(args) != 1:
usage(True)
tcp_server()
encoding = _get_arg(args, '-e', '--encoding')
progress = _get_arg(args, '-p', '--progress', flag=True)
double_sided = _get_arg(args, '-2', '--double-sided', flag=True)
double_paged = _get_arg(args, '-2', '--double-paged', flag=True)
if args == ['-']:
for line in sys.stdin:
_wrapper_convert(line.strip().split(';'), encoding=encoding, padding=double_sided, progress=progress)
_wrapper_convert(line.strip().split(';'), encoding=encoding, padding=double_paged, progress=progress)
elif len(args) < 2:
usage(True)
else:
_wrapper_convert(args, encoding=encoding, padding=double_sided, progress=progress)
_wrapper_convert(args, encoding=encoding, padding=double_paged, progress=progress)
if __name__ == '__main__':