pdfcrowd-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdfcrowd_cli/__init__.py +24 -0
- pdfcrowd_cli/__main__.py +28 -0
- pdfcrowd_cli/adapter.py +143 -0
- pdfcrowd_cli/catalog.py +46 -0
- pdfcrowd_cli/command-spec.json +16717 -0
- pdfcrowd_cli/entrypoints.py +228 -0
- pdfcrowd_cli/help.py +385 -0
- pdfcrowd_cli/parser.py +189 -0
- pdfcrowd_cli/state.py +368 -0
- pdfcrowd_cli/windows.py +173 -0
- pdfcrowd_cli-1.0.0.dist-info/LICENSE +22 -0
- pdfcrowd_cli-1.0.0.dist-info/METADATA +112 -0
- pdfcrowd_cli-1.0.0.dist-info/RECORD +16 -0
- pdfcrowd_cli-1.0.0.dist-info/WHEEL +5 -0
- pdfcrowd_cli-1.0.0.dist-info/entry_points.txt +10 -0
- pdfcrowd_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Copyright (C) 2009-2026 pdfcrowd.com
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person
|
|
4
|
+
# obtaining a copy of this software and associated documentation
|
|
5
|
+
# files (the "Software"), to deal in the Software without
|
|
6
|
+
# restriction, including without limitation the rights to use,
|
|
7
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
# copies of the Software, and to permit persons to whom the
|
|
9
|
+
# Software is furnished to do so, subject to the following
|
|
10
|
+
# conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be
|
|
13
|
+
# included in all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
17
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
19
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
20
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
"""One runtime boundary shared by CLI launchers and module invocation."""
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import traceback
|
|
29
|
+
from functools import wraps
|
|
30
|
+
|
|
31
|
+
from .catalog import CLIError, load_catalog
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def report_failure(message, error):
|
|
35
|
+
print(f'PDFCrowd: {message}', file=sys.stderr)
|
|
36
|
+
if os.environ.get('PDFCROWD_CLI_DEBUG') != '1':
|
|
37
|
+
return
|
|
38
|
+
print('Traceback (most recent call last):', file=sys.stderr)
|
|
39
|
+
# Report code locations without source text, locals, or exception values,
|
|
40
|
+
# which can contain credentials or private conversion inputs.
|
|
41
|
+
for frame in traceback.extract_tb(error.__traceback__):
|
|
42
|
+
print(f' File "{frame.filename}", line {frame.lineno}, '
|
|
43
|
+
f'in {frame.name}', file=sys.stderr)
|
|
44
|
+
if isinstance(error, SyntaxError) and error.filename:
|
|
45
|
+
print(f' File "{error.filename}", line {error.lineno}',
|
|
46
|
+
file=sys.stderr)
|
|
47
|
+
detail = type(error).__name__
|
|
48
|
+
if isinstance(error, ModuleNotFoundError):
|
|
49
|
+
detail += f': No module named {error.name!r}'
|
|
50
|
+
elif isinstance(error, OSError) and error.errno is not None:
|
|
51
|
+
detail += f': {os.strerror(error.errno)} (errno {error.errno})'
|
|
52
|
+
print(detail, file=sys.stderr)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def boundary(function):
|
|
56
|
+
@wraps(function)
|
|
57
|
+
def guarded(*args, **kwargs):
|
|
58
|
+
try:
|
|
59
|
+
result = function(*args, **kwargs)
|
|
60
|
+
sys.stdout.flush()
|
|
61
|
+
return result
|
|
62
|
+
except BrokenPipeError:
|
|
63
|
+
# Prevent a second broken-pipe diagnostic during interpreter exit.
|
|
64
|
+
try:
|
|
65
|
+
with open(os.devnull, 'wb') as sink:
|
|
66
|
+
os.dup2(sink.fileno(), sys.stdout.fileno())
|
|
67
|
+
except (OSError, ValueError):
|
|
68
|
+
pass
|
|
69
|
+
return 1
|
|
70
|
+
except KeyboardInterrupt:
|
|
71
|
+
print('PDFCrowd: Operation cancelled.', file=sys.stderr)
|
|
72
|
+
return 130
|
|
73
|
+
except CLIError as error:
|
|
74
|
+
print(f'{error.prefix}: {error}', file=sys.stderr)
|
|
75
|
+
return error.code
|
|
76
|
+
except (OSError, UnicodeError, ImportError) as error:
|
|
77
|
+
report_failure('Could not load the CLI or access local files. '
|
|
78
|
+
'Check the installation and file permissions.',
|
|
79
|
+
error)
|
|
80
|
+
return 4
|
|
81
|
+
except EOFError as error:
|
|
82
|
+
report_failure('Input ended before the command completed. '
|
|
83
|
+
'Run the command again to finish.', error)
|
|
84
|
+
return 4
|
|
85
|
+
except Exception as error:
|
|
86
|
+
report_failure('Command failed. Check the installation, input, '
|
|
87
|
+
'and options before retrying.', error)
|
|
88
|
+
return 4
|
|
89
|
+
return guarded
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def requested(arguments, flags):
|
|
93
|
+
before_end = arguments[:arguments.index('--')] if '--' in arguments else (
|
|
94
|
+
arguments)
|
|
95
|
+
return any(flag in before_end for flag in flags)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def auth_arguments(arguments):
|
|
99
|
+
if arguments[:2] == ['--non-interactive', 'auth']:
|
|
100
|
+
return ['auth'] + arguments[2:] + ['--non-interactive']
|
|
101
|
+
return arguments
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@boundary
|
|
105
|
+
def main(command, argv=None, program=None):
|
|
106
|
+
program = program or command
|
|
107
|
+
arguments = auth_arguments(list(sys.argv[1:] if argv is None else argv))
|
|
108
|
+
catalog = load_catalog()
|
|
109
|
+
converter = catalog['converters'].get(command)
|
|
110
|
+
if converter is None:
|
|
111
|
+
raise CLIError('Unknown converter. Use pdfcrowd --help.')
|
|
112
|
+
from . import help as discovery
|
|
113
|
+
from . import state
|
|
114
|
+
if arguments and arguments[0] == 'auth':
|
|
115
|
+
sys.stdout.write(state.authentication(
|
|
116
|
+
command, arguments[1:], catalog, program))
|
|
117
|
+
return 0
|
|
118
|
+
if arguments and arguments[0] == 'help':
|
|
119
|
+
sys.stdout.write(discovery.render(
|
|
120
|
+
converter, catalog, arguments[1:], program))
|
|
121
|
+
return 0
|
|
122
|
+
if not arguments or requested(arguments, ('--help', '-h')):
|
|
123
|
+
# Help remains available even with invalid options or local state.
|
|
124
|
+
sys.stdout.write(discovery.render(converter, catalog, [], program))
|
|
125
|
+
return 0
|
|
126
|
+
if requested(arguments, ('--version', '-V')):
|
|
127
|
+
print(f'PDFCrowd CLI {catalog["release"]["version"]}')
|
|
128
|
+
return 0
|
|
129
|
+
from .parser import make_parser, normalize, invocation_settings
|
|
130
|
+
normalized = normalize(arguments, converter, catalog, program)
|
|
131
|
+
# argparse's two-pass intermixed parser can reinterpret options after --.
|
|
132
|
+
end = normalized.index('--') if '--' in normalized else len(normalized)
|
|
133
|
+
namespace = make_parser(converter, catalog, program).parse_intermixed_args(
|
|
134
|
+
normalized[:end])
|
|
135
|
+
namespace.sources.extend(normalized[end + 1:])
|
|
136
|
+
if namespace.init_config:
|
|
137
|
+
sys.stdout.write(state.init_configuration(namespace, converter))
|
|
138
|
+
return 0
|
|
139
|
+
if not namespace.show_config and namespace.output == '-' and (
|
|
140
|
+
command != 'pdf2text' and sys.stdout.isatty()):
|
|
141
|
+
raise CLIError('Binary results cannot be displayed in a terminal. '
|
|
142
|
+
'Use --output FILE or redirect/pipe stdout.')
|
|
143
|
+
config_path, saved = state.configuration(namespace, converter, program)
|
|
144
|
+
supplied = invocation_settings(namespace, converter, program)
|
|
145
|
+
settings = dict(saved, **supplied)
|
|
146
|
+
origins = {name: 'invocation' if name in supplied else 'configuration'
|
|
147
|
+
for name in settings}
|
|
148
|
+
if namespace.show_config:
|
|
149
|
+
sys.stdout.write(state.inspect_settings(
|
|
150
|
+
converter, settings, origins, config_path, namespace.no_config))
|
|
151
|
+
return 0
|
|
152
|
+
from .adapter import convert, validate_sources
|
|
153
|
+
validate_sources(converter, namespace.sources)
|
|
154
|
+
settings = state.expand_text(converter, settings, origins, config_path)
|
|
155
|
+
credentials, source = state.select_credentials(command)
|
|
156
|
+
if source == 'demo':
|
|
157
|
+
print('Using the PDFCrowd demo account.\n'
|
|
158
|
+
f'{converter["demo_notice"]}\n'
|
|
159
|
+
'To use your own account, run: pdfcrowd auth login\n'
|
|
160
|
+
'Or set both PDFCROWD_USERNAME and PDFCROWD_API_KEY.',
|
|
161
|
+
file=sys.stderr)
|
|
162
|
+
output_info = [option for option in converter.get('output_info', [])
|
|
163
|
+
if getattr(namespace, option['name'].replace('-', '_'))]
|
|
164
|
+
convert(converter, settings, credentials, namespace.sources,
|
|
165
|
+
namespace.output, catalog, output_info,
|
|
166
|
+
invocation_options=supplied)
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@boundary
|
|
171
|
+
def cli_main(argv=None):
|
|
172
|
+
arguments = auth_arguments(list(sys.argv[1:] if argv is None else argv))
|
|
173
|
+
catalog = load_catalog()
|
|
174
|
+
from . import help as discovery
|
|
175
|
+
if arguments and arguments[0] == 'auth':
|
|
176
|
+
return main('html2pdf', arguments, 'pdfcrowd')
|
|
177
|
+
if not arguments or arguments in (['help'], ['--help'], ['-h']):
|
|
178
|
+
sys.stdout.write(discovery.root_overview(catalog))
|
|
179
|
+
return 0
|
|
180
|
+
if arguments in (['--version'], ['-V']):
|
|
181
|
+
print(f'PDFCrowd CLI {catalog["release"]["version"]}')
|
|
182
|
+
return 0
|
|
183
|
+
if arguments[:2] == ['help', '--json'] and len(arguments) == 2:
|
|
184
|
+
import json
|
|
185
|
+
print(json.dumps(catalog, indent=2))
|
|
186
|
+
return 0
|
|
187
|
+
if arguments[0] == 'help' and len(arguments) > 1:
|
|
188
|
+
return main(arguments[1], ['help'] + arguments[2:],
|
|
189
|
+
f'pdfcrowd {arguments[1]}')
|
|
190
|
+
if arguments[0] not in catalog['converters'] and requested(
|
|
191
|
+
arguments, ('--help', '-h')):
|
|
192
|
+
sys.stdout.write(discovery.root_overview(catalog))
|
|
193
|
+
return 0
|
|
194
|
+
return main(arguments[0], arguments[1:], f'pdfcrowd {arguments[0]}')
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def html2pdf():
|
|
198
|
+
return main('html2pdf')
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def html2image():
|
|
202
|
+
return main('html2image')
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def image2image():
|
|
206
|
+
return main('image2image')
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def pdf2pdf():
|
|
210
|
+
return main('pdf2pdf')
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def image2pdf():
|
|
214
|
+
return main('image2pdf')
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def pdf2html():
|
|
218
|
+
return main('pdf2html')
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def pdf2text():
|
|
222
|
+
return main('pdf2text')
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def pdf2image():
|
|
226
|
+
return main('pdf2image')
|
|
227
|
+
|
|
228
|
+
|
pdfcrowd_cli/help.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# Copyright (C) 2009-2026 pdfcrowd.com
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person
|
|
4
|
+
# obtaining a copy of this software and associated documentation
|
|
5
|
+
# files (the "Software"), to deal in the Software without
|
|
6
|
+
# restriction, including without limitation the rights to use,
|
|
7
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
# copies of the Software, and to permit persons to whom the
|
|
9
|
+
# Software is furnished to do so, subject to the following
|
|
10
|
+
# conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be
|
|
13
|
+
# included in all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
17
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
19
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
20
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
"""Offline text and JSON discovery from the packaged catalog."""
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import shutil
|
|
28
|
+
import textwrap
|
|
29
|
+
|
|
30
|
+
from .catalog import CLIError, command_options
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
TOPICS = {
|
|
34
|
+
'syntax': (
|
|
35
|
+
'Options use two leading dashes, for example --output FILE.\n\n'
|
|
36
|
+
'Boolean options accept true or false. Using a boolean flag alone '
|
|
37
|
+
'passes true; --option=false passes false.\n\n'
|
|
38
|
+
'For options with several arguments, supply each value separately '
|
|
39
|
+
'and quote values containing spaces or shell punctuation. For '
|
|
40
|
+
'example, an HTTP password containing a semicolon:\n'
|
|
41
|
+
" --http-auth username 'pass;word'\n\n"
|
|
42
|
+
'Use --option=VALUE when a value matches an option name. Compound '
|
|
43
|
+
'options can use a JSON array for such values:\n'
|
|
44
|
+
' --http-auth=\'["--help", "--version"]\'\n\n'
|
|
45
|
+
'Repeatable options can be supplied several times; their order is '
|
|
46
|
+
'preserved. Use -- before filenames starting with dashes. '
|
|
47
|
+
'Prefix filenames such as help or auth with ./.'),
|
|
48
|
+
'configuration': (
|
|
49
|
+
'Save conversion defaults in a separate config file for each '
|
|
50
|
+
'converter. The default file is read automatically when it exists. '
|
|
51
|
+
'Command-line options override saved settings. Commands do not '
|
|
52
|
+
'automatically save settings.\n\n'
|
|
53
|
+
'Create a starter file, then open it and uncomment the settings '
|
|
54
|
+
'you want to use:\n'
|
|
55
|
+
' {command} --init-config\n\n'
|
|
56
|
+
'Inspect saved defaults and command-line overrides:\n'
|
|
57
|
+
' {command} --show-config\n\n'
|
|
58
|
+
'Use --config PATH to select another file, or --no-config to ignore '
|
|
59
|
+
'saved conversion settings for one command. Saved credentials '
|
|
60
|
+
'still apply.\n\n'
|
|
61
|
+
'The file contains option-name = value lines, without leading '
|
|
62
|
+
'dashes or [section] headings. Compound settings use JSON arrays '
|
|
63
|
+
'of strings. Older semicolon-separated settings are also accepted. '
|
|
64
|
+
'For repeatable options, place additional values on indented '
|
|
65
|
+
'continuation lines.\n\n'
|
|
66
|
+
'HTML, CSS, and JavaScript options accept @{PATH} to read UTF-8 '
|
|
67
|
+
'text from a file. Relative paths in the config use the config file '
|
|
68
|
+
'directory; command-line paths use the current directory.\n\n'
|
|
69
|
+
'Keep credentials and other sensitive settings out of this file. '
|
|
70
|
+
'Use auth login or the credential environment variables.'),
|
|
71
|
+
'authentication': (
|
|
72
|
+
'Save your PDFCrowd username and API key for all converters:\n'
|
|
73
|
+
' {command} auth login\n\n'
|
|
74
|
+
'The API-key prompt hides your input. Use your API key, rather than '
|
|
75
|
+
'your account password.\n\n'
|
|
76
|
+
'For scripts, set both PDFCROWD_USERNAME and PDFCROWD_API_KEY. '
|
|
77
|
+
'To save that account without prompts:\n'
|
|
78
|
+
' {command} auth login --non-interactive\n\n'
|
|
79
|
+
'Check which credentials will be used, or remove the saved account:\n'
|
|
80
|
+
' {command} auth status\n'
|
|
81
|
+
' {command} auth logout\n\n'
|
|
82
|
+
'Status shows local settings; it does not validate the account '
|
|
83
|
+
'with the service. Logout removes saved credentials; environment '
|
|
84
|
+
'variables still apply.\n\n'
|
|
85
|
+
'Credentials come from environment variables first, then saved '
|
|
86
|
+
'credentials. Without either, conversions use the demo account '
|
|
87
|
+
'and produce marked output. To use demo even with a saved account:\n'
|
|
88
|
+
' PDFCROWD_USERNAME=demo PDFCROWD_API_KEY=demo \\\n'
|
|
89
|
+
' {command} -o example.pdf https://example.com\n\n'
|
|
90
|
+
'Conversions never prompt. Supply both credential environment '
|
|
91
|
+
'variables together. Invalid selected credentials cause an error; '
|
|
92
|
+
'the CLI will not retry with the demo account.'),
|
|
93
|
+
'input-output': (
|
|
94
|
+
'Supply an HTTP(S) URL, a local file, or - to read standard input. '
|
|
95
|
+
'pdf2pdf accepts local PDF files in the order supplied.\n\n'
|
|
96
|
+
'Save the result with --output FILE (or -o FILE). An existing file '
|
|
97
|
+
'is replaced only after success. Omitted output and --output - '
|
|
98
|
+
'write to standard output.\n\n'
|
|
99
|
+
'Only pdf2text writes directly to a terminal; other converters require '
|
|
100
|
+
'a file, pipe, or redirection. Diagnostics use stderr. '
|
|
101
|
+
'A failed stdout conversion can leave partial output; check the exit '
|
|
102
|
+
'status. Some converters produce ZIP archives containing '
|
|
103
|
+
'their results.'),
|
|
104
|
+
'exit-codes': (
|
|
105
|
+
'Use the exit status to check whether a command succeeded:\n\n'
|
|
106
|
+
' 0 Success\n'
|
|
107
|
+
' 1 The program reading the output pipe closed it\n'
|
|
108
|
+
' 2 Invalid arguments, conversion settings, or config file\n'
|
|
109
|
+
' 3 Authentication failed\n'
|
|
110
|
+
' 4 Conversion, file access, or installation failed\n'
|
|
111
|
+
' 130 Cancelled with Ctrl-C'),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def paragraph(text, indent=''):
|
|
116
|
+
width = max(40, min(100, shutil.get_terminal_size((80, 24)).columns))
|
|
117
|
+
return '\n'.join(
|
|
118
|
+
textwrap.fill(line, width=width, initial_indent=indent,
|
|
119
|
+
subsequent_indent=indent, break_on_hyphens=False,
|
|
120
|
+
break_long_words=False)
|
|
121
|
+
if line and not line.startswith(' ') else indent + line
|
|
122
|
+
for line in text.splitlines())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def signature(option):
|
|
126
|
+
if len(option['arguments']) == 1 and (
|
|
127
|
+
option['arguments'][0]['type'] == 'bool'):
|
|
128
|
+
return f'--{option["name"]}[=true|false]'
|
|
129
|
+
suffix = ' '.join(arg['name'].upper() for arg in option['arguments'])
|
|
130
|
+
return f'--{option["name"]} {suffix}'
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def control_signature(option):
|
|
134
|
+
suffix = f' {option["metavar"]}' if 'metavar' in option else ''
|
|
135
|
+
return ', '.join(option['flags']) + suffix
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def control_detail(option, command, converter_name, include_signature=True):
|
|
139
|
+
lines = [paragraph(option['description'])]
|
|
140
|
+
if include_signature:
|
|
141
|
+
lines[:0] = [control_signature(option), '']
|
|
142
|
+
if option.get('usage'):
|
|
143
|
+
lines.extend(['', 'Usage: ' + option['usage'].format(command=command)])
|
|
144
|
+
if 'default' in option and option.get('action') != 'store_true':
|
|
145
|
+
default = option.get('default_description', option['default'])
|
|
146
|
+
lines.extend(['', f'Default: {default}'])
|
|
147
|
+
for text in option.get('details', []):
|
|
148
|
+
lines.extend(['', paragraph(text)])
|
|
149
|
+
if option.get('scope'):
|
|
150
|
+
lines.extend(['', f'Use this option with {option["scope"]}.'])
|
|
151
|
+
if option.get('topic') == 'configuration':
|
|
152
|
+
from .state import paths
|
|
153
|
+
config, _ = paths(converter_name)
|
|
154
|
+
lines.extend(['', f'Default config file: {config}'])
|
|
155
|
+
if option.get('examples'):
|
|
156
|
+
lines.extend(['', 'Examples'])
|
|
157
|
+
lines.extend(' ' + example.format(command=command)
|
|
158
|
+
for example in option['examples'])
|
|
159
|
+
if option.get('topic'):
|
|
160
|
+
lines.extend(['', f'More help: {command} help {option["topic"]}'])
|
|
161
|
+
return '\n'.join(lines)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def detail(option, include_signature=True):
|
|
165
|
+
lines = [paragraph(option['description'])]
|
|
166
|
+
if include_signature:
|
|
167
|
+
lines[:0] = [signature(option), '']
|
|
168
|
+
for argument in option['arguments']:
|
|
169
|
+
default = argument['default']
|
|
170
|
+
kind = {'bool': 'true or false', 'int': 'whole number',
|
|
171
|
+
'str': 'text'}[argument['type']]
|
|
172
|
+
label = argument['name'].replace('_', ' ')
|
|
173
|
+
label = label.upper() if label in ('css', 'html', 'url', 'dpi') else (
|
|
174
|
+
label.capitalize())
|
|
175
|
+
lines.extend(['', f'{label}: {kind}'])
|
|
176
|
+
if argument['description']:
|
|
177
|
+
lines.append(paragraph(argument['description']))
|
|
178
|
+
if default is not None:
|
|
179
|
+
displayed = str(default).lower() if isinstance(default, bool) else (
|
|
180
|
+
default)
|
|
181
|
+
lines.append(f'Default: {displayed}')
|
|
182
|
+
if argument['allowed_values']:
|
|
183
|
+
values = argument['allowed_values']
|
|
184
|
+
if any(item['description'] for item in values):
|
|
185
|
+
lines.append('Allowed values:')
|
|
186
|
+
for item in values:
|
|
187
|
+
text = str(item['value'])
|
|
188
|
+
if item['description']:
|
|
189
|
+
text += f': {item["description"]}'
|
|
190
|
+
lines.append(paragraph(text, indent=' '))
|
|
191
|
+
else:
|
|
192
|
+
names = ', '.join(str(item['value']) for item in values)
|
|
193
|
+
lines.append(paragraph(f'Allowed values: {names}'))
|
|
194
|
+
lines.extend(paragraph(hint) for hint in argument['constraints']
|
|
195
|
+
if not (argument['allowed_values'] and
|
|
196
|
+
hint.startswith('Allowed values')))
|
|
197
|
+
if option['repeatable']:
|
|
198
|
+
lines.extend(['', paragraph(
|
|
199
|
+
'You can repeat this option. Command-line values replace the '
|
|
200
|
+
'saved list and keep their supplied order.')])
|
|
201
|
+
if option['sensitive']:
|
|
202
|
+
lines.extend(['', paragraph(
|
|
203
|
+
'Sensitive values are hidden by --show-config. Supply this option '
|
|
204
|
+
'on the command line; it cannot be saved in the conversion '
|
|
205
|
+
'config file.')])
|
|
206
|
+
if option['text_file']:
|
|
207
|
+
lines.extend(['', 'Use @{PATH} to read UTF-8 text from a file.'])
|
|
208
|
+
if len(option['arguments']) == 1 and (
|
|
209
|
+
option['arguments'][0]['type'] == 'bool'):
|
|
210
|
+
lines.extend(['', f'Use --{option["name"]} for true, or '
|
|
211
|
+
f'--{option["name"]}=false for false.'])
|
|
212
|
+
if option['warnings']:
|
|
213
|
+
lines.append(paragraph(option['warnings']))
|
|
214
|
+
if option['deprecated']:
|
|
215
|
+
lines.append('Deprecated; consult the converter reference.')
|
|
216
|
+
return '\n'.join(lines)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def render(converter, catalog, arguments, program=None):
|
|
220
|
+
name = converter['name']
|
|
221
|
+
command = program or name
|
|
222
|
+
if arguments == ['--json']:
|
|
223
|
+
result = dict(catalog, converters={name: converter})
|
|
224
|
+
from .state import paths
|
|
225
|
+
config, credentials = paths(name)
|
|
226
|
+
result['invocation'] = {
|
|
227
|
+
'converter': name, 'command': command, 'config_path': str(config),
|
|
228
|
+
'credentials_path': str(credentials)}
|
|
229
|
+
return json.dumps(result, indent=2) + '\n'
|
|
230
|
+
if len(arguments) > 1:
|
|
231
|
+
raise CLIError(f'Choose one help topic, for example '
|
|
232
|
+
f'{command} help configuration. '
|
|
233
|
+
f'For JSON, run {command} help --json.')
|
|
234
|
+
topic = arguments[0] if arguments else None
|
|
235
|
+
if topic and topic.startswith('--'):
|
|
236
|
+
topic = topic[2:]
|
|
237
|
+
if topic is None:
|
|
238
|
+
return overview(converter, catalog, command)
|
|
239
|
+
if topic in TOPICS:
|
|
240
|
+
text = TOPICS[topic]
|
|
241
|
+
if topic == 'input-output' and name == 'pdf2pdf':
|
|
242
|
+
_, rest = text.split('\n\n', 1)
|
|
243
|
+
text = ('Supply one or more existing local PDF files. Files are '
|
|
244
|
+
'processed in the order supplied.\n\n' + rest)
|
|
245
|
+
if topic == 'syntax' and not any(option['name'] == 'http-auth'
|
|
246
|
+
for option in converter['options']):
|
|
247
|
+
text = text.replace('an HTTP password', 'a text value')
|
|
248
|
+
text = text.replace("--http-auth username 'pass;word'",
|
|
249
|
+
"--option 'value;word'")
|
|
250
|
+
text = text.replace('--http-auth', '--option')
|
|
251
|
+
result = paragraph(text.replace('{command}', command))
|
|
252
|
+
if topic in ('configuration', 'authentication'):
|
|
253
|
+
from .state import paths
|
|
254
|
+
config, credentials = paths(name)
|
|
255
|
+
result += (f'\n\nDefault config file: {config}\n'
|
|
256
|
+
f'Shared credentials file: {credentials}')
|
|
257
|
+
if topic == 'authentication':
|
|
258
|
+
auth = catalog['authentication']
|
|
259
|
+
result += (f'\nAPI key: {auth["api_key_url"]}\n'
|
|
260
|
+
f'{converter["demo_notice"]}')
|
|
261
|
+
return result + '\n'
|
|
262
|
+
options = converter['options']
|
|
263
|
+
categories = {option['category']['id']: option['category']['name']
|
|
264
|
+
for option in options if not option['hidden']}
|
|
265
|
+
if topic == 'categories':
|
|
266
|
+
return ('Help topics\n syntax: How to supply arguments\n'
|
|
267
|
+
' configuration: Save and load conversion defaults\n'
|
|
268
|
+
' authentication: Set up your PDFCrowd account\n'
|
|
269
|
+
' input-output: Read inputs and save results\n'
|
|
270
|
+
' exit-codes: Check whether a command succeeded\n'
|
|
271
|
+
' json: Machine-readable help for scripts and AI agents\n'
|
|
272
|
+
'\nConversion option groups\n' +
|
|
273
|
+
'\n'.join(f' {key}: {value}'
|
|
274
|
+
for key, value in categories.items()) +
|
|
275
|
+
f'\n\nUse {command} help CATEGORY or help OPTION.\n')
|
|
276
|
+
if topic == 'all' or topic in categories:
|
|
277
|
+
selected = [option for option in options if not option['hidden'] and (
|
|
278
|
+
topic == 'all' or option['category']['id'] == topic)]
|
|
279
|
+
controls = [control_detail(option, command, name)
|
|
280
|
+
for option in command_options(converter, catalog)]
|
|
281
|
+
details = [detail(option) for option in selected]
|
|
282
|
+
return '\n\n'.join(
|
|
283
|
+
(controls if topic == 'all' else []) + details) + '\n'
|
|
284
|
+
for option in command_options(converter, catalog):
|
|
285
|
+
if option['name'] == topic or topic in option['flags']:
|
|
286
|
+
return control_detail(option, command, name) + '\n'
|
|
287
|
+
for option in options:
|
|
288
|
+
if option['name'] == topic:
|
|
289
|
+
return detail(option) + '\n'
|
|
290
|
+
raise CLIError(f'Unknown help topic. Run {command} help categories '
|
|
291
|
+
f'to list topics, or {command} help all for every option.')
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def overview(converter, catalog, program=None):
|
|
295
|
+
name = converter['name']
|
|
296
|
+
command = program or name
|
|
297
|
+
from .state import paths
|
|
298
|
+
config, _ = paths(name)
|
|
299
|
+
sources = 'SOURCE [SOURCE ...]' if name == 'pdf2pdf' else 'SOURCE'
|
|
300
|
+
input_file = 'input.pdf' if name.startswith('pdf') else 'input.png'
|
|
301
|
+
example = f'{command} --output result {input_file}'
|
|
302
|
+
if name == 'html2pdf':
|
|
303
|
+
example = f'{command} --output result.pdf https://example.com'
|
|
304
|
+
elif name == 'pdf2pdf':
|
|
305
|
+
example = f'{command} first.pdf --output result.pdf second.pdf'
|
|
306
|
+
elif name == 'html2image':
|
|
307
|
+
example = f'{command} --output result.png https://example.com'
|
|
308
|
+
elif name in ('image2pdf', 'pdf2text'):
|
|
309
|
+
suffix = 'pdf' if name == 'image2pdf' else 'txt'
|
|
310
|
+
example = f'{command} --output result.{suffix} {input_file}'
|
|
311
|
+
input_description = ('One or more local PDF files, in the supplied order.'
|
|
312
|
+
if name == 'pdf2pdf' else
|
|
313
|
+
'An HTTP(S) URL, an existing file, or - for stdin.')
|
|
314
|
+
lines = [f'PDFCrowd: {converter["description"]}',
|
|
315
|
+
f'Guide: {converter["guide_url"]}', '',
|
|
316
|
+
f'Usage: {command} [options] {sources}',
|
|
317
|
+
f'Input: {input_description}', '', 'Command options']
|
|
318
|
+
for option in command_options(converter, catalog):
|
|
319
|
+
if option.get('topic') == 'configuration' or option.get('scope'):
|
|
320
|
+
continue
|
|
321
|
+
lines.append(f' {control_signature(option)}')
|
|
322
|
+
lines.append(f' {option["description"]}')
|
|
323
|
+
lines.extend(['', 'Examples', f' {example}'])
|
|
324
|
+
common = {'page-size', 'orientation', 'page-margins', 'no-margins',
|
|
325
|
+
'viewport-width', 'header-html', 'footer-html', 'custom-css',
|
|
326
|
+
'custom-javascript', 'output-format', 'pages', 'scale-factor'}
|
|
327
|
+
common_options = [f' {signature(option)}'
|
|
328
|
+
for option in converter['options']
|
|
329
|
+
if option['name'] in common]
|
|
330
|
+
if common_options:
|
|
331
|
+
lines.extend(['', 'Common conversion options'] + common_options)
|
|
332
|
+
lines.extend(['', f'See all options: {command} help all', '',
|
|
333
|
+
'Authentication',
|
|
334
|
+
f' {command} auth login Save your account',
|
|
335
|
+
f' {command} auth status Show which account is used',
|
|
336
|
+
f' {command} auth logout Remove the saved account',
|
|
337
|
+
' Or set PDFCROWD_USERNAME and PDFCROWD_API_KEY.',
|
|
338
|
+
' Credentials: environment, saved account, then demo.',
|
|
339
|
+
' Scripts: auth login --non-interactive requires both '
|
|
340
|
+
'environment variables.',
|
|
341
|
+
f' {converter["demo_notice"]}', '', 'Configuration',
|
|
342
|
+
f' Default file: {config}',
|
|
343
|
+
' Saved conversion defaults are loaded automatically.',
|
|
344
|
+
' Commands do not automatically save conversion settings.',
|
|
345
|
+
' Saved credentials are managed separately.', ''])
|
|
346
|
+
for option in catalog['command_options']:
|
|
347
|
+
if option.get('topic') == 'configuration':
|
|
348
|
+
lines.append(f' {control_signature(option)}')
|
|
349
|
+
lines.append(f' {option["description"]}')
|
|
350
|
+
lines.extend(['', f' More details: {command} help configuration',
|
|
351
|
+
'', 'More help',
|
|
352
|
+
f' {command} help all List every option',
|
|
353
|
+
f' {command} help OPTION Explain one option',
|
|
354
|
+
f' {command} help categories List help topics and groups',
|
|
355
|
+
f' {command} help --json JSON for scripts and AI '
|
|
356
|
+
'agents'])
|
|
357
|
+
return '\n'.join(lines) + '\n'
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def root_overview(catalog):
|
|
361
|
+
lines = ['PDFCrowd CLI', '',
|
|
362
|
+
'Usage: pdfcrowd CONVERTER [options] SOURCE',
|
|
363
|
+
' pdfcrowd auth ACTION',
|
|
364
|
+
' pdfcrowd help CONVERTER [TOPIC]', '',
|
|
365
|
+
'Available converters']
|
|
366
|
+
lines.extend(f' {name}: {converter["description"]}'
|
|
367
|
+
for name, converter in catalog['converters'].items())
|
|
368
|
+
lines.extend(['', 'Account setup',
|
|
369
|
+
' auth login Save your PDFCrowd username and API key',
|
|
370
|
+
' auth status Show which credentials will be used',
|
|
371
|
+
' auth logout Remove saved credentials',
|
|
372
|
+
' Credentials are shared by all converter commands.',
|
|
373
|
+
' Without personal credentials, conversions use the demo '
|
|
374
|
+
'account.', '', 'Examples',
|
|
375
|
+
' pdfcrowd html2pdf -o example.pdf https://example.com',
|
|
376
|
+
' pdfcrowd pdf2pdf first.pdf -o merged.pdf second.pdf',
|
|
377
|
+
' pdfcrowd auth login',
|
|
378
|
+
' pdfcrowd html2pdf help all', '',
|
|
379
|
+
'Options',
|
|
380
|
+
' --help, -h Show this overview',
|
|
381
|
+
' --version, -V Show the PDFCrowd CLI version', '',
|
|
382
|
+
'Machine-readable help for scripts and AI agents',
|
|
383
|
+
' pdfcrowd help --json',
|
|
384
|
+
'Module invocation: python -m pdfcrowd_cli ...'])
|
|
385
|
+
return '\n'.join(lines) + '\n'
|