Compare commits

...

17 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
4 changed files with 119 additions and 55 deletions
+1
View File
@@ -1,3 +1,4 @@
.idea
__pycache__
*.exe
*.spec
+1
View File
@@ -4,4 +4,5 @@ pyinstaller ^
--console ^
--icon "NONE" ^
--name "WinziPrint" ^
--clean ^
src/winziprint.py
+2 -2
View File
@@ -14,10 +14,10 @@ winziprint [-h] [-v] [-d DIR] [ -D | [-p] [-2] [-e ENCODING] [ - | INPUT [INPUT.
Command line options:
* `-h`, `--help` - show help message and exit
* `-V`, `--version` - show version and exit
* `-D`, `--daemon` - run as a daemon and expose a named socket
* `-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-sided` - pad documents to an event number of pages
* `-2`, `--double-paged` - pad documents to an even number of pages
* `-p`, `--progress` - show progress updates
Command line arguments:
+115 -53
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import TextIO
from typing import TextIO, Callable
import os
import sys
import time
@@ -10,69 +10,117 @@ 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,
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}', file=out, 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 < len(html_files):
print(f'progress: {i + BATCH_SIZE + i // BATCH_SIZE + 1}/{steps}', file=out, 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:
@@ -80,8 +128,7 @@ def convert(input_files: list[str],
if os.path.isfile(pdf):
os.remove(pdf)
if progress:
print(f'progress: {steps}/{steps}', file=out, flush=True)
next_step()
return page_nums
@@ -116,7 +163,7 @@ def _wrapper_convert(args: list[str],
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',
file=out, flush=True)
@@ -128,6 +175,22 @@ def _wrapper_convert(args: list[str],
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] [-d DIR] [ -D | [-p] [-2] [-e ENCODING] [ - | INPUT [INPUT...] OUTPUT ] ]\n'
'\n'
@@ -137,7 +200,7 @@ def usage(error: bool = False) -> None:
' -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'
@@ -175,12 +238,23 @@ def _get_arg(args: list[str], n1: str, n2: str = None, flag: bool = False) -> No
class ConnectionHandler(socketserver.StreamRequestHandler):
def handle(self):
try:
while True:
out = io.TextIOWrapper(self.wfile, encoding='utf-8')
for line in io.TextIOWrapper(self.rfile, encoding='utf-8'):
_wrapper_convert(line.strip().split(';'), out=out)
except ValueError:
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:
@@ -195,33 +269,21 @@ def main() -> None:
os.chdir(working_dir)
if '-D' in args:
print('Running as daemon')
if len(args) != 1:
usage(True)
# 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)
try:
server.serve_forever()
except KeyboardInterrupt:
print('', file=sys.stderr)
print('Shutting down')
return
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__':