Compare commits
19 Commits
a76d540bae
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cf11791dad | |||
| 1cf775beec | |||
| 9cab6f0a00 | |||
| 3e0d004e82 | |||
| 1a425915d7 | |||
| ad12481e50 | |||
| 640e025c2d | |||
| f2f3e10e2a | |||
| 75689983df | |||
| 7945e0ae8a | |||
| 8f0588e8f3 | |||
| 4d5020f2b9 | |||
| 0d014a4b34 | |||
| 0ec9092df5 | |||
| 40c1b68d51 | |||
| 3abb764ca3 | |||
| 81848ac767 | |||
| 5b3b96fb35 | |||
| cd6b30cf80 |
@@ -1,3 +1,4 @@
|
|||||||
.idea
|
.idea
|
||||||
__pycache__
|
__pycache__
|
||||||
*.exe
|
*.exe
|
||||||
|
*.spec
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
pyinstaller --noconfirm ^
|
pyinstaller ^
|
||||||
|
--noconfirm ^
|
||||||
--onefile ^
|
--onefile ^
|
||||||
--console ^
|
--console ^
|
||||||
--icon "NONE" ^
|
--icon "NONE" ^
|
||||||
--name "WinziPrint" ^
|
--name "WinziPrint" ^
|
||||||
|
--clean ^
|
||||||
src/winziprint.py
|
src/winziprint.py
|
||||||
|
|||||||
@@ -3,3 +3,43 @@
|
|||||||
|
|
||||||
A standalone [WeasyPrint](https://weasyprint.org/) wrapper for Windows.
|
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
@@ -1,73 +1,126 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from typing import Union
|
from typing import TextIO, Callable
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import gc
|
import gc
|
||||||
|
import socketserver
|
||||||
|
import io
|
||||||
|
import signal
|
||||||
|
import math
|
||||||
|
import threading
|
||||||
|
|
||||||
import weasyprint
|
import weasyprint
|
||||||
import pypdf
|
import pypdf
|
||||||
|
|
||||||
|
|
||||||
VERSION = __version__ = '0.1.0'
|
VERSION = __version__ = '0.3.0'
|
||||||
|
SOCKET_ADDRESS = ('127.0.0.1', 30983)
|
||||||
|
|
||||||
BATCH_SIZE = 10
|
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],
|
def convert(input_files: list[str],
|
||||||
output_files: str,
|
output_file: str,
|
||||||
encoding: str = None,
|
encoding: str = None,
|
||||||
padding: bool = False,
|
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
|
# it takes roughly 100ms to generate one document
|
||||||
tmp_page_nums = []
|
|
||||||
tmp_file_names = []
|
tmp_file_names = []
|
||||||
page_nums = []
|
page_nums = []
|
||||||
|
|
||||||
html_files = [file.lstrip('!') for file in input_files if not file.endswith('.pdf')]
|
html_files = [file.lstrip('!#') for file in input_files if not file.endswith('.pdf')]
|
||||||
steps = len(html_files) + len(html_files) // BATCH_SIZE + 1
|
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:
|
try:
|
||||||
|
tmp_page_nums = []
|
||||||
for i in range(0, len(html_files), BATCH_SIZE):
|
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]
|
batch = html_files[i:i + BATCH_SIZE]
|
||||||
documents = []
|
tmp_page_nums += convert_part(tmp_file, batch, next_step, encoding=encoding)
|
||||||
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)
|
|
||||||
|
|
||||||
|
letterhead = None
|
||||||
merger = pypdf.PdfWriter()
|
merger = pypdf.PdfWriter()
|
||||||
i = 0
|
i = 0
|
||||||
for n, file_name in enumerate(input_files):
|
for n, file_name in enumerate(input_files):
|
||||||
p0 = len(merger.pages)
|
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'):
|
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:
|
else:
|
||||||
batch_page_nums = tmp_page_nums[i // BATCH_SIZE * BATCH_SIZE:(i // BATCH_SIZE + 1) * BATCH_SIZE]
|
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])
|
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
|
i += 1
|
||||||
p1 = len(merger.pages)
|
p1 = len(merger.pages)
|
||||||
page_nums.append(p1 - p0)
|
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.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()
|
merger.close()
|
||||||
del merger
|
del merger
|
||||||
finally:
|
finally:
|
||||||
@@ -75,52 +128,84 @@ def convert(input_files: list[str],
|
|||||||
if os.path.isfile(pdf):
|
if os.path.isfile(pdf):
|
||||||
os.remove(pdf)
|
os.remove(pdf)
|
||||||
|
|
||||||
if progress:
|
next_step()
|
||||||
print(f'progress: {steps}/{steps}', flush=True)
|
|
||||||
|
|
||||||
return page_nums
|
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:
|
try:
|
||||||
if len(args) < 2:
|
if len(args) < 2:
|
||||||
print(f'error: Too few arguments', flush=True)
|
print(f'error: Too few arguments', file=out, flush=True)
|
||||||
return
|
return
|
||||||
inputs = args[:-1]
|
inputs = args[:-1]
|
||||||
output = args[-1]
|
output = args[-1]
|
||||||
if inputs[0] == '-2':
|
while len(inputs) > 0:
|
||||||
inputs.pop(0)
|
if inputs[0] == '-2':
|
||||||
padding = True
|
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()
|
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)
|
total = sum(p + 1 if padding and p % 2 != 0 else p for p in pages)
|
||||||
t1 = time.process_time()
|
t1 = time.process_time()
|
||||||
print(f'success: '
|
print(f'success: '
|
||||||
f'{len(args) - 1} documents, '
|
f'{len(inputs)} documents, '
|
||||||
f'{total} pages ({", ".join(str(p) for p in pages)}), '
|
f'{total} pages ({", ".join(str(p) for p in pages)}), '
|
||||||
f'{t1 - t0:.1f} sec',
|
f'{t1 - t0:.1f} sec',
|
||||||
flush=True)
|
file=out, flush=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = str(e).replace('\n', ' ')
|
msg = str(e).replace('\n', ' ')
|
||||||
print(f'error: {msg}', flush=True)
|
print(f'error: {msg}', file=out, flush=True)
|
||||||
traceback.print_exception(e)
|
traceback.print_exception(e, file=sys.stderr)
|
||||||
finally:
|
finally:
|
||||||
gc.collect()
|
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:
|
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'
|
'options:\n'
|
||||||
' -h, --help show this help message and exit\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'
|
' -d, --directory set the working directory\n'
|
||||||
' -e, --encoding encoding of the input files\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'
|
' -p, --progress show progress updates\n'
|
||||||
'\n'
|
'\n'
|
||||||
' - use stdin for retrieving input and output file names (semi-colon-seperated)\n'
|
' - use stdin for retrieving input and output file names (semi-colon-seperated)\n'
|
||||||
' INPUT name of an html input file\n'
|
' INPUT name of a html input file\n'
|
||||||
' OUTPUT name of an pdf output file',
|
' OUTPUT name of a pdf output file',
|
||||||
file=sys.stderr if error else sys.stdout)
|
file=sys.stderr if error else sys.stdout)
|
||||||
sys.exit(1 if error else 0)
|
sys.exit(1 if error else 0)
|
||||||
|
|
||||||
@@ -132,7 +217,7 @@ def version() -> None:
|
|||||||
sys.exit(0)
|
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
|
v = None
|
||||||
for n in [n1] + (n2 and [n2] or []):
|
for n in [n1] + (n2 and [n2] or []):
|
||||||
if flag:
|
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
|
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:
|
def main() -> None:
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
if len(args) == 0 or '-h' in args or '--help' in args:
|
if len(args) == 0 or '-h' in args or '--help' in args:
|
||||||
usage()
|
usage()
|
||||||
elif '-v' in args or '--version' in args:
|
elif '-V' in args or '--version' in args:
|
||||||
version()
|
version()
|
||||||
|
|
||||||
working_dir = _get_arg(args, '-d', '--directory')
|
working_dir = _get_arg(args, '-d', '--directory')
|
||||||
if working_dir:
|
if working_dir:
|
||||||
os.chdir(working_dir)
|
os.chdir(working_dir)
|
||||||
|
|
||||||
|
if '-D' in args:
|
||||||
|
if len(args) != 1:
|
||||||
|
usage(True)
|
||||||
|
tcp_server()
|
||||||
|
|
||||||
encoding = _get_arg(args, '-e', '--encoding')
|
encoding = _get_arg(args, '-e', '--encoding')
|
||||||
progress = _get_arg(args, '-p', '--progress', flag=True)
|
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 == ['-']:
|
if args == ['-']:
|
||||||
for line in sys.stdin:
|
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:
|
elif len(args) < 2:
|
||||||
usage(True)
|
usage(True)
|
||||||
else:
|
else:
|
||||||
_wrapper_convert(args, encoding=encoding, padding=double_sided, progress=progress)
|
_wrapper_convert(args, encoding=encoding, padding=double_paged, progress=progress)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user