conver 0.1.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.
conver/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """
2
+ Convenient public API for the conver package.
3
+ """
4
+
5
+ from .conver import conver
6
+ from .conver import ConverError
7
+ from .conver import InputFileNotFound
8
+ from .conver import UnsupportedFormat
9
+ from .conver import WordStartError
10
+ from .conver import SaveError
11
+ from .conver import IPCError
12
+ from .conver import PlatformNotSupported
13
+ from .__version__ import __version__
14
+
15
+ __all__ = [
16
+ "conver",
17
+ "ConverError",
18
+ "InputFileNotFound",
19
+ "UnsupportedFormat",
20
+ "WordStartError",
21
+ "SaveError",
22
+ "IPCError",
23
+ "PlatformNotSupported",
24
+ "__version__",
25
+ ]
conver/__version__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
conver/_convert.py ADDED
@@ -0,0 +1,193 @@
1
+ # Author: Timur Ulyahin, https://github.com/ucomru
2
+ # License: MIT – provided "as-is" without any warranty or liability
3
+ # Copyright: (c) 2024 Timur Ulyahin
4
+
5
+ """
6
+ MODULE OVERVIEW:
7
+ This module implements the low-level IPC-based `convert` function used to perform
8
+ document format conversion by invoking platform-specific scripts for macOS (JXA)
9
+ and Windows (PowerShell).
10
+
11
+ It serves as the internal transport layer between Python and the operating-system
12
+ automation scripts, enabling the conversion of Microsoft Word documents (.doc, .docx)
13
+ and other supported formats into output formats such as PDF, RTF, TXT, HTML, and more.
14
+
15
+ The function returns structured information (status, message, error_code) that
16
+ is interpreted by the high-level `conver()` API, which converts these results
17
+ into Python exceptions on failure.
18
+
19
+ USAGE:
20
+ To use the `convert` function, specify the `input_path`, `output_path`, and optionally
21
+ `keep_open` to indicate whether the application should remain open post-conversion.
22
+
23
+ RECOMMENDED PATHS:
24
+ Use paths like "~/Downloads/" on macOS or "C:/Users/YourName/Downloads/" on Windows
25
+ for both `input_path` and `output_path` to ensure permission consistency.
26
+
27
+ SETUP (macOS):
28
+ 1. Go to System Settings > Privacy & Security > Files and Folders.
29
+ 2. Under Microsoft Word or Microsoft Excel, enable access to "Downloads/" if required.
30
+
31
+ SUPPORTED FORMATS:
32
+ The function supports conversion of the following file extensions:
33
+ - Microsoft Word (.docx) // Modern Word document format
34
+ - Word 97-2003 (.doc) // Legacy Word document format
35
+ - PDF (.pdf) // Export documents to PDF format
36
+ - Rich Text Format (.rtf) // Basic document format with limited formatting
37
+ - OpenDocument Text (.odt) // OpenOffice/LibreOffice document format
38
+ - Plain Text (.txt) // Plain text format without any formatting
39
+ - HTML (.html) // Web page format for viewing in browsers
40
+
41
+ STATUS SCHEMA (ConvertResult):
42
+ The function returns a structured dictionary as follows:
43
+ {
44
+ "status": "success" | "error", // Execution status
45
+ "input": "<path to input file>", // Path to the input file (may be null in error cases)
46
+ "output": "<path to output file>", // Path to the output file (may be null in error cases)
47
+ "message": "OK" | "<error message>", // "OK" on success or specific error message
48
+ "error_code": 0 | <error code> // 0 for success; specific code for different errors
49
+ }
50
+
51
+ ERROR CODES:
52
+ The `convert` function and associated scripts define a set of error codes for troubleshooting:
53
+ 0 - Success
54
+ 1 - Incorrect JSON format or missing required fields
55
+ 2 - Input file format is unsupported
56
+ 3 - Output file format is unsupported
57
+ 11 - Input file not found
58
+ 21 - Microsoft Word or Excel did not start within the expected time
59
+ 31 - Error saving or converting file
60
+ 98 - Invalid JSON output from script
61
+ 99 - Unsupported platform
62
+ """
63
+
64
+ import subprocess
65
+ from json import loads, dumps, JSONDecodeError
66
+ import sys
67
+ from importlib.resources import files, as_file
68
+ from typing import TypedDict, Optional, List
69
+
70
+
71
+ class ConvertResult(TypedDict):
72
+ status: str
73
+ input: Optional[str]
74
+ output: Optional[str]
75
+ message: str
76
+ error_code: int
77
+
78
+
79
+ def convert(
80
+ input_path: str, output_path: str, keep_open: bool = False
81
+ ) -> ConvertResult:
82
+ """
83
+ Convert a document from one format to another using platform-specific scripts.
84
+
85
+ This function acts as an interface for inter-process communication (IPC) with macOS
86
+ or Windows scripts to perform document conversions. Depending on the operating system,
87
+ it triggers a JXA or PowerShell script, which performs the conversion and returns a
88
+ structured JSON result. The `convert` function processes the JSON result, ensuring
89
+ it matches the strict structure defined by `ConvertResult`.
90
+
91
+ Parameters:
92
+ input_path (str): The path to the source file.
93
+ output_path (str): The path where the converted file will be saved.
94
+ keep_open (bool): Whether to keep the application (Word/Excel) open after processing.
95
+
96
+ Returns:
97
+ ConvertResult: A dictionary with keys for `status`, `input`, `output`, `message`, and
98
+ `error_code`, providing details of the conversion process and any errors encountered.
99
+
100
+ Errors:
101
+ Returns structured error responses as defined by the ConvertResult type. Unexpected errors
102
+ that are not captured by the script will return error_code 98 for JSON parsing failures.
103
+ """
104
+ if sys.platform == "darwin":
105
+ return _run_macos_script(input_path, output_path, keep_open)
106
+ elif sys.platform == "win32":
107
+ return _run_windows_script(input_path, output_path, keep_open)
108
+ else:
109
+ # Unsupported platform
110
+ return {
111
+ "status": "error",
112
+ "input": input_path,
113
+ "output": output_path,
114
+ "message": "Unsupported platform.",
115
+ "error_code": 99,
116
+ }
117
+
118
+
119
+ def _run_macos_script(
120
+ input_path: str, output_path: str, keep_open: bool
121
+ ) -> ConvertResult:
122
+ """Run macOS JXA script for conversion."""
123
+ with as_file(files("conver.scripts").joinpath("convert.jxa")) as script_path:
124
+ command = [
125
+ "osascript",
126
+ "-l",
127
+ "JavaScript",
128
+ str(script_path),
129
+ dumps({"input": input_path, "output": output_path, "keepOpen": keep_open}),
130
+ ]
131
+ return _execute_command(command, input_path, output_path)
132
+
133
+
134
+ def _run_windows_script(
135
+ input_path: str, output_path: str, keep_open: bool
136
+ ) -> ConvertResult:
137
+ """Run Windows PowerShell script for conversion."""
138
+ with as_file(files("conver.scripts").joinpath("convert.ps1")) as script_path:
139
+ command = [
140
+ "powershell",
141
+ "-ExecutionPolicy",
142
+ "Bypass",
143
+ "-File",
144
+ str(script_path),
145
+ "-jsonArgs",
146
+ dumps({"input": input_path, "output": output_path, "keepOpen": keep_open}),
147
+ ]
148
+ return _execute_command(command, input_path, output_path)
149
+
150
+
151
+ def _execute_command(
152
+ command: List[str], input_path: str, output_path: str
153
+ ) -> ConvertResult:
154
+ """
155
+ Executes the command for either macOS or Windows and parses the JSON output.
156
+
157
+ Attempts to run the provided script command and expects JSON output for successful completion.
158
+ In cases where the script exits with an error, the `error_code` from the script’s JSON response
159
+ is returned directly to help identify specific issues. If JSON parsing fails, a general error
160
+ code of 98 is returned.
161
+
162
+ Parameters:
163
+ command (List[str]): The command to be executed.
164
+ input_path (str): The path to the input file, passed for error context.
165
+ output_path (str): The path to the output file, passed for error context.
166
+
167
+ Returns:
168
+ ConvertResult: A structured dictionary with status, message, error code, input, and output.
169
+ """
170
+ result = subprocess.run(command, capture_output=True, text=True)
171
+
172
+ # osascript places all output in stderr, including success
173
+ raw = result.stdout.strip() or result.stderr.strip()
174
+
175
+ try:
176
+ data = loads(raw)
177
+ except JSONDecodeError:
178
+ return {
179
+ "status": "error",
180
+ "input": input_path,
181
+ "output": output_path,
182
+ "message": "Invalid JSON output from script.",
183
+ "error_code": 98,
184
+ }
185
+
186
+ # canonical normalized result
187
+ return {
188
+ "status": data.get("status", "error"),
189
+ "input": data.get("input", input_path),
190
+ "output": data.get("output", output_path),
191
+ "message": data.get("message", ""),
192
+ "error_code": data.get("error_code", result.returncode),
193
+ }
conver/cli.py ADDED
@@ -0,0 +1,90 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ from click import (
5
+ command,
6
+ argument,
7
+ option,
8
+ version_option,
9
+ echo,
10
+ Path as ClickPath,
11
+ UsageError,
12
+ )
13
+
14
+ from .conver import conver, ConverError
15
+ from .__version__ import __version__
16
+
17
+
18
+ def fail(label: str, err: Exception, code: int = 1):
19
+ echo(f"{label} {err}", err=True)
20
+ sys.exit(code)
21
+
22
+
23
+ @command(help="Convert Word documents.")
24
+ @argument(
25
+ "inputs", type=ClickPath(exists=True, path_type=Path), nargs=-1, metavar="INPUT..."
26
+ )
27
+ @option(
28
+ "-o",
29
+ "--output",
30
+ "output",
31
+ type=ClickPath(path_type=Path),
32
+ required=False,
33
+ help="Output file or directory (for multiple inputs).",
34
+ )
35
+ @option("-p", "--pdf", "target", flag_value="pdf", help="Convert to PDF.")
36
+ @option("-d", "--docx", "target", flag_value="docx", help="Convert to DOCX.")
37
+ @option("-r", "--rtf", "target", flag_value="rtf", help="Convert to RTF.")
38
+ @option("-t", "--txt", "target", flag_value="txt", help="Convert to TXT.")
39
+ @option("-h", "--html", "target", flag_value="html", help="Convert to HTML.")
40
+ @option("-k", "--keep-open", is_flag=True, help="Keep Microsoft Word open.")
41
+ @version_option(__version__, "-V", "--version")
42
+ def cli(inputs, output, target, keep_open):
43
+ """Main CLI entry point."""
44
+ if not inputs:
45
+ raise UsageError("No input files specified.")
46
+
47
+ if output is not None and target is not None:
48
+ raise UsageError("Cannot use both --output and format flags together.")
49
+
50
+ # --- MULTIPLE INPUTS ---
51
+ if len(inputs) > 1:
52
+ if output is None:
53
+ raise UsageError("Multiple inputs require --output DIRECTORY.")
54
+
55
+ if output.exists() and not output.is_dir():
56
+ raise UsageError("--output must be a directory for multiple inputs.")
57
+
58
+ output.mkdir(parents=True, exist_ok=True)
59
+
60
+ if target is None:
61
+ target = "pdf"
62
+
63
+ for inp in inputs:
64
+ out_file = output / (inp.stem + f".{target}")
65
+ try:
66
+ result = conver(inp, out_file, keep_open=keep_open)
67
+ echo(result)
68
+ except ConverError as err:
69
+ fail("Error:", err, err.error_code or 1)
70
+
71
+ # --- SINGLE INPUT ---
72
+ else:
73
+ inp = inputs[0]
74
+
75
+ if output is not None:
76
+ try:
77
+ result = conver(inp, output, keep_open=keep_open)
78
+ echo(result)
79
+ except ConverError as err:
80
+ fail("Error:", err, err.error_code or 1)
81
+ else:
82
+ if target is None:
83
+ target = "pdf"
84
+
85
+ out_file = inp.with_suffix("." + target)
86
+ try:
87
+ result = conver(inp, out_file, keep_open=keep_open)
88
+ echo(result)
89
+ except ConverError as err:
90
+ fail("Error:", err, err.error_code or 1)
conver/conver.py ADDED
@@ -0,0 +1,154 @@
1
+ # Author: Timur Ulyahin, https://github.com/ucomru
2
+ # License: MIT – provided "as-is" without any warranty or liability
3
+ # Copyright: (c) 2024 Timur Ulyahin
4
+
5
+ """
6
+ High-level document conversion API.
7
+
8
+ This module exposes the `conver()` function — a Pythonic wrapper around the
9
+ low-level IPC-based converter. It normalizes paths, performs minimal validation,
10
+ invokes the underlying platform-specific script, and raises structured exceptions
11
+ on failure.
12
+
13
+ Successful conversion returns the resolved output path as a `Path` object.
14
+ Errors are represented as subclasses of `ConverError`.
15
+ """
16
+
17
+ from typing import Union, Tuple
18
+ from pathlib import Path
19
+
20
+ from ._convert import convert
21
+
22
+
23
+ class ConverError(Exception):
24
+ def __init__(self, message, error_code=None):
25
+ super().__init__(message)
26
+ self.error_code = error_code
27
+
28
+
29
+ class InputFileNotFound(ConverError):
30
+ pass
31
+
32
+
33
+ class UnsupportedFormat(ConverError):
34
+ pass
35
+
36
+
37
+ class WordStartError(ConverError):
38
+ pass
39
+
40
+
41
+ class SaveError(ConverError):
42
+ pass
43
+
44
+
45
+ class IPCError(ConverError):
46
+ pass
47
+
48
+
49
+ class PlatformNotSupported(ConverError):
50
+ pass
51
+
52
+
53
+ _ERROR_MAP = {
54
+ 1: IPCError,
55
+ 2: UnsupportedFormat,
56
+ 3: UnsupportedFormat,
57
+ 11: InputFileNotFound,
58
+ 21: WordStartError,
59
+ 31: SaveError,
60
+ 98: IPCError,
61
+ 99: PlatformNotSupported,
62
+ }
63
+
64
+
65
+ def _normalize_paths(
66
+ input_path: Union[str, Path],
67
+ output_path: Union[str, Path],
68
+ ) -> Tuple[Path, Path]:
69
+ """
70
+ Normalize and resolve input/output paths.
71
+
72
+ Rules:
73
+ - input_path is converted to absolute Path.
74
+ - output_path:
75
+ * if only a filename is provided, it is placed into the same directory
76
+ as input_path;
77
+ * otherwise, it is resolved relative to the filesystem.
78
+ - All returned paths are absolute.
79
+ """
80
+
81
+ # Normalize input
82
+ in_path = Path(input_path).expanduser().absolute()
83
+
84
+ # Normalize output raw
85
+ out_raw = Path(output_path).expanduser()
86
+
87
+ # filename-only -> drop into input directory
88
+ if out_raw.parent == Path("."):
89
+ out_path = in_path.parent / out_raw
90
+ else:
91
+ out_path = out_raw
92
+
93
+ out_path = out_path.absolute()
94
+
95
+ return in_path, out_path
96
+
97
+
98
+ def conver(
99
+ input_path: Union[str, Path],
100
+ output_path: Union[str, Path],
101
+ keep_open: bool = False,
102
+ ) -> Path:
103
+ """
104
+ Convert a document to another format via the platform-level converter.
105
+
106
+ Parameters
107
+ ----------
108
+ input_path : str or pathlib.Path
109
+ Path to the source document.
110
+ output_path : str or pathlib.Path
111
+ Target output path with desired extension.
112
+ keep_open : bool, default=False
113
+ Whether to keep Microsoft Word open after conversion.
114
+
115
+ Returns
116
+ -------
117
+ Path
118
+ The resolved absolute output path.
119
+
120
+ Raises
121
+ ------
122
+ InputFileNotFound
123
+ The input file does not exist.
124
+ UnsupportedFormat
125
+ The source or target format is not supported by the underlying converter.
126
+ WordStartError
127
+ Microsoft Word could not be started by the script.
128
+ SaveError
129
+ The document could not be saved in the target format.
130
+ IPCError
131
+ Communication with the script failed or produced invalid JSON.
132
+ PlatformNotSupported
133
+ The current OS is not supported.
134
+ """
135
+
136
+ in_path, out_path = _normalize_paths(input_path, output_path)
137
+
138
+ if not in_path.exists():
139
+ raise InputFileNotFound(f"Input file does not exist: {in_path}", 11)
140
+
141
+ result = convert(
142
+ input_path=str(in_path),
143
+ output_path=str(out_path),
144
+ keep_open=keep_open,
145
+ )
146
+
147
+ code = result.get("error_code", None)
148
+ msg = result.get("message", "Unknown error")
149
+
150
+ if code and code != 0:
151
+ exc_class = _ERROR_MAP.get(code, ConverError)
152
+ raise exc_class(f"[{code}] {msg}", code)
153
+
154
+ return out_path
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/osascript -l JavaScript
2
+
3
+ /*
4
+ Author: Timur Ulyahin, https://github.com/ucomru
5
+ License: MIT – provided "as-is" without any warranty or liability
6
+ Copyright: (c) 2024 Timur Ulyahin
7
+
8
+ USAGE:
9
+ osascript -l JavaScript convert.jxa \
10
+ '{"input": "<inputPath>", "output": "<outputPath>", "keepOpen": true | false}'
11
+
12
+ RECOMMENDED PATHS:
13
+ Use "~/Downloads/" for both <inputPath> and <outputPath>.
14
+
15
+ SETUP (macOS):
16
+ 1. Go to System Settings > Privacy & Security > Files and Folders
17
+ 2. Under Microsoft Word, enable access to "Downloads Folder".
18
+
19
+ SUPPORTED FORMATS:
20
+ The script supports the following file extensions for input and output:
21
+ - Microsoft Word (.docx) // Modern Word document format
22
+ - Word 97-2003 (.doc) // Legacy Word document format
23
+ - PDF (.pdf) // Export documents to PDF format
24
+ - Rich Text Format (.rtf) // Basic document format with limited formatting
25
+ - OpenDocument Text (.odt) // OpenOffice/LibreOffice document format
26
+ - Plain Text (.txt) // Plain text format without any formatting
27
+ - HTML (.html) // Web page format for viewing in browsers
28
+
29
+ PARAMETERS:
30
+ - input: "<path to input file>" // Path to the input file (required)
31
+ - output: "<path to output file>" // Path to the output file (required)
32
+ - keepOpen: true | false // Optional. Leave Word open after processing (default: false)
33
+
34
+ STATUS SCHEMA (JSON Output):
35
+ The script outputs a JSON object with the following fields:
36
+ {
37
+ "status": "success" | "error", // Execution status
38
+ "input": "<path to input file>", // Path to the input file
39
+ "output": "<path to output file>", // Path to the output file (null if error)
40
+ "message": "OK" | "<error message>", // "OK" on success or specific error message
41
+ "error_code": 0 | <error code> // 0 for success; specific code for different errors
42
+ }
43
+
44
+ ERROR CODES:
45
+ 0 - Success
46
+ 1 - Incorrect JSON format or missing required fields
47
+ 2 - Input file format is unsupported
48
+ 3 - Output file format is unsupported
49
+ 11 - Input file not found
50
+ 21 - Microsoft Word did not start within the expected time
51
+ 31 - Error saving or converting file
52
+ */
53
+
54
+ ObjC.import("stdlib");
55
+ const SystemEvents = Application("System Events");
56
+
57
+ // Supported file formats with their corresponding codes in Word
58
+ const formatCodes = {
59
+ "docx": 16,
60
+ "doc": 0,
61
+ "pdf": 17,
62
+ "rtf": 6,
63
+ "odt": 19,
64
+ "txt": 7,
65
+ "html": 8
66
+ };
67
+
68
+ // Retrieve the format code based on file extension, if supported
69
+ function getFormatCodeByExtension(extension) {
70
+ const ext = extension.toLowerCase();
71
+ return ext in formatCodes ? formatCodes[ext] : null;
72
+ }
73
+
74
+ function run(argv) {
75
+ // Ensure there is only one JSON argument passed to the script
76
+ if (argv.length !== 1) {
77
+ console.log(JSON.stringify({
78
+ "status": "error",
79
+ "input": null,
80
+ "output": null,
81
+ "message": "A single JSON argument is required.",
82
+ "error_code": 1
83
+ }));
84
+ $.exit(1);
85
+ }
86
+
87
+ let params;
88
+ // Attempt to parse the JSON input
89
+ try {
90
+ params = JSON.parse(argv[0]);
91
+ } catch (error) {
92
+ console.log(JSON.stringify({
93
+ "status": "error",
94
+ "input": null,
95
+ "output": null,
96
+ "message": "Invalid JSON format.",
97
+ "error_code": 1
98
+ }));
99
+ $.exit(1);
100
+ }
101
+
102
+ const inputPath = params.input;
103
+ let outputPath = params.output;
104
+ const keepOpen = params.keepOpen === true; // Flag to optionally keep Word open
105
+
106
+ // Validate the presence of required fields
107
+ if (!inputPath || !outputPath) {
108
+ console.log(JSON.stringify({
109
+ "status": "error",
110
+ "input": inputPath || null,
111
+ "output": outputPath || null,
112
+ "message": "Both 'input' and 'output' fields are required in JSON.",
113
+ "error_code": 1
114
+ }));
115
+ $.exit(1);
116
+ }
117
+
118
+ // Check if input file format is supported
119
+ const inputExtension = inputPath.split('.').pop().toLowerCase();
120
+ const inputFormat = getFormatCodeByExtension(inputExtension);
121
+
122
+ if (inputFormat === null) {
123
+ console.log(JSON.stringify({
124
+ "status": "error",
125
+ "input": inputPath,
126
+ "output": null,
127
+ "message": `The input file format "${inputExtension}" is unsupported.`,
128
+ "error_code": 2
129
+ }));
130
+ $.exit(2);
131
+ }
132
+
133
+ // Check if output file format is supported
134
+ const outputExtension = outputPath.split('.').pop().toLowerCase();
135
+ const outputFormat = getFormatCodeByExtension(outputExtension);
136
+
137
+ if (outputFormat === null) {
138
+ console.log(JSON.stringify({
139
+ "status": "error",
140
+ "input": inputPath,
141
+ "output": outputPath,
142
+ "message": `The output file format "${outputExtension}" is unsupported.`,
143
+ "error_code": 3
144
+ }));
145
+ $.exit(3);
146
+ }
147
+
148
+ const Word = Application("Microsoft Word");
149
+ Word.includeStandardAdditions = true;
150
+
151
+ let wordStartedByScript = false; // Flag to track if Word was started by this script
152
+
153
+ try {
154
+ // Check if the input file exists
155
+ const fileManager = $.NSFileManager.defaultManager;
156
+ if (!fileManager.fileExistsAtPath($(inputPath))) {
157
+ console.log(JSON.stringify({
158
+ "status": "error",
159
+ "input": inputPath,
160
+ "output": null,
161
+ "message": `File "${inputPath}" not found.`,
162
+ "error_code": 11
163
+ }));
164
+ $.exit(11);
165
+ }
166
+
167
+ // Activate or launch Word if not already running
168
+ if (!Word.running()) {
169
+ Word.activate();
170
+ wordStartedByScript = true; // Mark that Word was launched by this script
171
+
172
+ let attempt = 0;
173
+ // Wait for Word to start, with a maximum number of attempts
174
+ while (!Word.running() && attempt < 10) {
175
+ delay(0.1);
176
+ attempt++;
177
+ }
178
+
179
+ // Check if Word started successfully
180
+ if (Word.running()) {
181
+ SystemEvents.processes["Microsoft Word"].visible = false;
182
+ } else {
183
+ console.log(JSON.stringify({
184
+ "status": "error",
185
+ "input": inputPath,
186
+ "output": outputPath,
187
+ "message": "Microsoft Word did not start within the expected time.",
188
+ "error_code": 21
189
+ }));
190
+ $.exit(21);
191
+ }
192
+ } else {
193
+ Word.launch();
194
+ SystemEvents.processes["Microsoft Word"].visible = false;
195
+ }
196
+
197
+ // Open the document and save it in the desired format
198
+ Word.open(inputPath);
199
+ const doc = Word.documents[0];
200
+ doc.saveAs({
201
+ fileName: outputPath,
202
+ fileFormat: outputFormat
203
+ });
204
+ doc.close({ saving: "no" }); // Close the document without saving any changes
205
+ } catch (error) {
206
+ console.log(JSON.stringify({
207
+ "status": "error",
208
+ "input": inputPath,
209
+ "output": outputPath,
210
+ "message": error.toString(),
211
+ "error_code": 31
212
+ }));
213
+ $.exit(31);
214
+ }
215
+
216
+ // Output success message in JSON format
217
+ console.log(JSON.stringify({
218
+ "status": "success",
219
+ "input": inputPath,
220
+ "output": outputPath,
221
+ "message": "OK",
222
+ "error_code": 0
223
+ }));
224
+
225
+ // Close Word if it was started by this script and keepOpen is false
226
+ if (wordStartedByScript && !keepOpen) {
227
+ Word.quit();
228
+ }
229
+ }
@@ -0,0 +1,190 @@
1
+ <#
2
+ Author: Timur Ulyahin, https://github.com/ucomru
3
+ License: MIT – provided "as-is" without any warranty or liability
4
+ Copyright: (c) 2024 Timur Ulyahin
5
+
6
+ USAGE:
7
+ powershell -ExecutionPolicy Bypass -File convert.ps1 -jsonArgs "{\"input\": \"<inputPath>\", \"output\": \"<outputPath>\", \"keepOpen\": true | false}"
8
+
9
+ RECOMMENDED PATHS:
10
+ Use paths like "C:\Users\UserName\Downloads\" for both <inputPath> and <outputPath>.
11
+
12
+ SETUP (Windows):
13
+ 1. Ensure Microsoft Word is installed on your system.
14
+ 2. If prompted, allow PowerShell to automate Microsoft Word.
15
+
16
+ SUPPORTED FORMATS:
17
+ The script supports the following file extensions for input and output:
18
+ - Microsoft Word (.docx) # Modern Word document format
19
+ - Word 97-2003 (.doc) # Legacy Word document format
20
+ - PDF (.pdf) # Export documents to PDF format
21
+ - Rich Text Format (.rtf) # Basic document format with limited formatting
22
+ - OpenDocument Text (.odt) # OpenOffice/LibreOffice document format
23
+ - Plain Text (.txt) # Plain text format without any formatting
24
+ - HTML (.html) # Web page format for viewing in browsers
25
+
26
+ PARAMETERS:
27
+ - input: "<path to input file>" # Path to the input file (required)
28
+ - output: "<path to output file>" # Path to the output file (required)
29
+ - keepOpen: true | false # Optional. Leave Word open after processing (default: false)
30
+
31
+ STATUS SCHEMA (JSON Output):
32
+ The script outputs a JSON object with the following fields:
33
+ {
34
+ "status": "success" | "error", # Execution status
35
+ "input": "<path to input file>", # Path to the input file
36
+ "output": "<path to output file>", # Path to the output file (null if error)
37
+ "message": "OK" | "<error message>", # "OK" on success or specific error message
38
+ "error_code": 0 | <error code> # 0 for success; specific code for different errors
39
+ }
40
+
41
+ ERROR CODES:
42
+ 0 - Success
43
+ 1 - Incorrect JSON format or missing required fields
44
+ 2 - Input file format is unsupported
45
+ 3 - Output file format is unsupported
46
+ 11 - Input file not found
47
+ 21 - Microsoft Word did not start or is not installed
48
+ 31 - Error saving or converting file
49
+ #>
50
+
51
+ param (
52
+ [string]$jsonArgs
53
+ )
54
+
55
+ # Supported file formats and corresponding codes for Microsoft Word
56
+ $formatCodes = @{
57
+ "docx" = 16
58
+ "doc" = 0
59
+ "pdf" = 17
60
+ "rtf" = 6
61
+ "odt" = 19
62
+ "txt" = 7
63
+ "html" = 8
64
+ }
65
+
66
+ # Function to retrieve format code by extension
67
+ function Get-FormatCodeByExtension {
68
+ param ($extension)
69
+ $extension = $extension.ToLower()
70
+ return $formatCodes[$extension] -as [int]
71
+ }
72
+
73
+ # Parse JSON input
74
+ try {
75
+ $params = $jsonArgs | ConvertFrom-Json
76
+ } catch {
77
+ Write-Output (ConvertTo-Json @{
78
+ status = "error"
79
+ input = $null
80
+ output = $null
81
+ message = "Invalid JSON format."
82
+ error_code = 1
83
+ })
84
+ exit 1
85
+ }
86
+
87
+ $inputPath = $params.input
88
+ $outputPath = $params.output
89
+ $keepOpen = $params.keepOpen -eq $true
90
+
91
+ # Validate required parameters
92
+ if (-not $inputPath -or -not $outputPath) {
93
+ Write-Output (ConvertTo-Json @{
94
+ status = "error"
95
+ input = $inputPath
96
+ output = $outputPath
97
+ message = "Both 'input' and 'output' fields are required in JSON."
98
+ error_code = 1
99
+ })
100
+ exit 1
101
+ }
102
+
103
+ # Check input file format support
104
+ $inputExtension = [System.IO.Path]::GetExtension($inputPath).TrimStart('.').ToLower()
105
+ $inputFormat = Get-FormatCodeByExtension -extension $inputExtension
106
+
107
+ if (-not $inputFormat) {
108
+ Write-Output (ConvertTo-Json @{
109
+ status = "error"
110
+ input = $inputPath
111
+ output = $null
112
+ message = "The input file format '$inputExtension' is unsupported."
113
+ error_code = 2
114
+ })
115
+ exit 2
116
+ }
117
+
118
+ # Check output file format support
119
+ $outputExtension = [System.IO.Path]::GetExtension($outputPath).TrimStart('.').ToLower()
120
+ $outputFormat = Get-FormatCodeByExtension -extension $outputExtension
121
+
122
+ if ($outputFormat -eq $null) {
123
+ Write-Output (ConvertTo-Json @{
124
+ status = "error"
125
+ input = $inputPath
126
+ output = $outputPath
127
+ message = "The output file format '$outputExtension' is unsupported."
128
+ error_code = 3
129
+ })
130
+ exit 3
131
+ }
132
+
133
+ # Initialize Word COM object with error handling
134
+ try {
135
+ $word = New-Object -ComObject Word.Application -ErrorAction Stop
136
+ } catch {
137
+ Write-Output (ConvertTo-Json @{
138
+ status = "error"
139
+ input = $inputPath
140
+ output = $outputPath
141
+ message = "Microsoft Word is not installed or cannot be started."
142
+ error_code = 21
143
+ })
144
+ exit 21
145
+ }
146
+
147
+ $word.Visible = $false
148
+ $wordStartedByScript = $false
149
+
150
+ try {
151
+ # Check if input file exists
152
+ if (-not (Test-Path -Path $inputPath)) {
153
+ Write-Output (ConvertTo-Json @{
154
+ status = "error"
155
+ input = $inputPath
156
+ output = $null
157
+ message = "File '$inputPath' not found."
158
+ error_code = 11
159
+ })
160
+ exit 11
161
+ }
162
+
163
+ # Open and convert the document
164
+ $doc = $word.Documents.Open($inputPath)
165
+ $doc.SaveAs([ref]$outputPath, [ref]$outputFormat)
166
+ $doc.Close()
167
+ } catch {
168
+ Write-Output (ConvertTo-Json @{
169
+ status = "error"
170
+ input = $inputPath
171
+ output = $outputPath
172
+ message = $_.Exception.Message
173
+ error_code = 31
174
+ })
175
+ exit 31
176
+ }
177
+
178
+ # Success message
179
+ Write-Output (ConvertTo-Json @{
180
+ status = "success"
181
+ input = $inputPath
182
+ output = $outputPath
183
+ message = "OK"
184
+ error_code = 0
185
+ })
186
+
187
+ # Close Word if opened by the script and keepOpen is false
188
+ if (-not $keepOpen) {
189
+ $word.Quit()
190
+ }
@@ -0,0 +1,353 @@
1
+ Metadata-Version: 2.4
2
+ Name: conver
3
+ Version: 0.1.0
4
+ Summary: A cross-platform document converter.
5
+ Author-email: Timur Ulyahin <ucomru@pm.me>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ucomru/python-conver
8
+ Project-URL: Repository, https://github.com/ucomru/python-conver
9
+ Project-URL: Documentation, https://github.com/ucomru/python-conver
10
+ Project-URL: Issues, https://github.com/ucomru/python-conver/issues
11
+ Keywords: conversion,documents,office,tools,cli
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: click
19
+ Dynamic: license-file
20
+
21
+ # conver — Cross-platform Word Document Converter
22
+
23
+ `conver` is a cross-platform Python package that converts Microsoft Word documents
24
+ using native system automation:
25
+
26
+ - macOS: JXA (JavaScript for Automation)
27
+ - Windows: PowerShell + Word COM automation
28
+
29
+ The package provides:
30
+
31
+ 1. A high-level Python API: `conver.conver()`
32
+ 2. A command-line interface: `conver`
33
+ 3. A low-level IPC layer calling platform scripts
34
+ (`convert.jxa` for macOS, `convert.ps1` for Windows)
35
+
36
+ Conversion is performed via Microsoft Word itself, ensuring maximum compatibility
37
+ with `.docx`, `.doc`, `.rtf`, `.txt`, `.html`, `.odt`.
38
+
39
+ ---
40
+
41
+ ## Features
42
+
43
+ - Cross-platform: macOS and Windows
44
+ - Uses the actual Microsoft Word engine
45
+ - Clean, minimal Python API
46
+ - CLI with direct input/output or format-selection flags
47
+ - Structured error handling via custom exceptions
48
+ - Filename-only outputs automatically placed next to the input file
49
+ - Safe low-level IPC layer between Python and native automation scripts
50
+
51
+ ## Installation
52
+
53
+ ### Using pip
54
+
55
+ ```bash
56
+ pip install conver
57
+ ```
58
+
59
+ ### Recommended for CLI use: pipx
60
+
61
+ If you primarily use `conver` as a command-line tool, it is best installed with **pipx**.
62
+ This isolates the package in its own virtual environment and avoids polluting your system Python:
63
+
64
+ ```bash
65
+ pipx install conver
66
+ ```
67
+
68
+ After installation:
69
+
70
+ ```bash
71
+ conver --help
72
+ ```
73
+
74
+ will be available globally.
75
+
76
+ ---
77
+
78
+ ### Requirements
79
+
80
+ - Python **3.9+**
81
+ - Microsoft Word must be installed on the system
82
+ - macOS (JXA) or Windows (PowerShell + Word COM automation)
83
+
84
+ ---
85
+
86
+ ## Python Usage
87
+
88
+ ### Basic example
89
+
90
+ ```python
91
+ from conver import conver
92
+
93
+ conver("document.docx", "document.pdf")
94
+ ```
95
+
96
+ Returns:
97
+
98
+ ```python
99
+ Path("/absolute/path/document.pdf")
100
+ ```
101
+
102
+ ### Filename-only output
103
+
104
+ ```python
105
+ conver("/Users/me/docs/a.docx", "a.pdf")
106
+ ```
107
+
108
+ Automatically produces:
109
+
110
+ ```bash
111
+ /Users/me/docs/a.pdf
112
+ ```
113
+
114
+ ### Error handling example
115
+
116
+ ```python
117
+ from conver import conver, UnsupportedFormat
118
+
119
+ try:
120
+ conver("a.doc", "a.xyz")
121
+ except UnsupportedFormat:
122
+ print("This output format is not supported.")
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Python API Reference
128
+
129
+ ### `conver(input_path, output_path, keep_open=False) -> pathlib.Path`
130
+
131
+ High-level document conversion function.
132
+
133
+ #### Parameters
134
+
135
+ - `input_path` — `str` or `pathlib.Path`
136
+ Path to the source document.
137
+ - `output_path` — `str` or `pathlib.Path`
138
+ Target output filename or full path.
139
+ - `keep_open` — `bool`
140
+ Whether to keep Microsoft Word open after conversion.
141
+
142
+ #### Returns
143
+ - `pathlib.Path` — absolute path of the output file.
144
+
145
+ #### Raises
146
+
147
+ - `InputFileNotFound`
148
+ - `UnsupportedFormat`
149
+ - `WordStartError`
150
+ - `SaveError`
151
+ - `IPCError`
152
+ - `PlatformNotSupported`
153
+
154
+ ---
155
+
156
+ ## CLI Usage
157
+
158
+ The CLI supports two modes: direct output path, or format flags.
159
+
160
+ ### 1. Explicit input/output
161
+
162
+ ```bash
163
+ conver input.docx output.pdf
164
+ ```
165
+
166
+ ### 2. Format flags (output placed next to input)
167
+
168
+ ```bash
169
+ conver input.docx --pdf
170
+ conver input.docx --rtf
171
+ conver input.docx --txt
172
+ conver input.docx --html
173
+ conver input.docx --docx
174
+ ```
175
+
176
+ If only one input file and no explicit output is given,
177
+ the selected format determines the extension:
178
+
179
+ ```
180
+ input.docx --pdf -> input.pdf
181
+ input.docx --rtf -> input.rtf
182
+ ```
183
+
184
+ ### Output Behavior
185
+
186
+ When OUTPUT is omitted, the resulting file is written next to the INPUT file.
187
+ Examples:
188
+
189
+ ```
190
+ conver a.docx --pdf -> a.pdf
191
+ conver /path/x.docx -> /path/x.pdf
192
+ ```
193
+
194
+ When multiple inputs are provided, --output must be a directory.
195
+ If the directory does not exist, it will be created automatically.
196
+
197
+ ### Default Format
198
+
199
+ If no format flag is provided and no explicit output path is given,
200
+ the default output format is **PDF**:
201
+
202
+ ```bash
203
+ conver input.docx # -> input.pdf
204
+ ```
205
+
206
+ ### CLI Syntax
207
+
208
+ ```
209
+ Usage:
210
+ conver <input> <output>
211
+ conver <input> [--pdf | --docx | --rtf | --txt | --html] [--keep-open]
212
+
213
+ Examples:
214
+ conver a.docx a.pdf
215
+ conver a.docx --pdf
216
+ conver /path/to/file.doc --html
217
+ ```
218
+
219
+ ### CLI Arguments
220
+
221
+ `input`
222
+ One or more input files. Patterns like `*.docx` are expanded by the shell.
223
+
224
+ `output`
225
+ Optional.
226
+ For single input: output filename.
227
+ For multiple inputs: must be a directory.
228
+
229
+ ---
230
+
231
+ ### Multiple Inputs (Batch Mode)
232
+
233
+ The CLI can process multiple input files at once:
234
+
235
+ ```bash
236
+ conver *.docx -o outdir
237
+ conver file1.docx file2.docx file3.docx -o converted/
238
+ ```
239
+
240
+ Rules:
241
+
242
+ - When multiple inputs are provided, `--output` **must** point to a directory.
243
+ - If the directory does not exist, it will be created automatically.
244
+ - If no format flag is provided, the default output format is **PDF**.
245
+ - Format flags (`--pdf`, `--rtf`, etc.) **cannot be used together with** `--output FILE`.
246
+ - Globbing (`*.docx`) is expanded by your shell before reaching the CLI.
247
+
248
+ Output filenames follow the pattern:
249
+
250
+ ```
251
+ input.docx -> outdir/input.pdf
252
+ ```
253
+
254
+ ### Shell Globbing
255
+
256
+ Patterns like `*.docx` are expanded by your shell before the `conver` command is executed.
257
+
258
+ Examples:
259
+ `conver *.docx -o outdir`
260
+ `conver path/*.rtf --pdf`
261
+
262
+ This means the CLI receives the expanded list of files as separate arguments.
263
+
264
+ ### Format-Flag Restrictions
265
+
266
+ Format-selection flags:
267
+
268
+ ```
269
+ --pdf --docx --rtf --txt --html
270
+ ```
271
+
272
+ work **only when OUTPUT is omitted**, i.e.:
273
+
274
+ *Allowed*:
275
+ ```bash
276
+ conver input.docx --pdf
277
+ conver input.docx output.pdf
278
+ ```
279
+
280
+ *Invalid:
281
+ ```bash
282
+ conver input.docx output.pdf --pdf
283
+ ```
284
+
285
+ ---
286
+
287
+ ## Supported Formats
288
+
289
+ Conversion capabilities depend on Microsoft Word.
290
+
291
+ Input formats: `.docx`, `.doc`, `.pdf`, `.rtf`, `.odt`, `.txt`, `.html`
292
+
293
+ Output formats: `.pdf`, `.docx`, `.doc`, `.pdf`, `.rtf`, `.odt`, `.txt`, `.html`
294
+
295
+ ---
296
+
297
+ ## Platform Notes
298
+
299
+ ### macOS (JXA)
300
+
301
+ The script `convert.jxa` runs through:
302
+
303
+ ```
304
+ osascript -l JavaScript
305
+ ```
306
+
307
+ macOS may require granting Microsoft Word file-access permissions.
308
+
309
+ ### Windows (PowerShell)
310
+
311
+ The script `convert.ps1` uses:
312
+
313
+ ```
314
+ Word.Application COM automation
315
+ ```
316
+
317
+ If Word prompts the user, automation must be allowed.
318
+
319
+ ---
320
+
321
+ ## Low-Level Error Codes (Reference)
322
+
323
+ These codes come from platform scripts and are mapped to exceptions by `conver()`:
324
+
325
+ | Code | Meaning |
326
+ |------|--------------------------------------|
327
+ | 0 | Success |
328
+ | 1 | Invalid JSON from stdin |
329
+ | 2 | Unsupported input format |
330
+ | 3 | Unsupported output format |
331
+ | 11 | Input file not found |
332
+ | 21 | Word startup timeout |
333
+ | 31 | Word could not save the file |
334
+ | 98 | Script produced invalid JSON |
335
+ | 99 | Unsupported platform |
336
+
337
+ ---
338
+
339
+ ## License
340
+
341
+ MIT License
342
+ (c) 2024 Timur Ulyahin
343
+ https://github.com/ucomru
344
+
345
+ ---
346
+
347
+ ## Project Homepage
348
+
349
+ GitHub:
350
+ https://github.com/ucomru/python-conver
351
+
352
+ PyPI:
353
+ https://pypi.org/project/conver/
@@ -0,0 +1,13 @@
1
+ conver/__init__.py,sha256=40DZmYJLiEt1HR6tZDm_ZjfhMvpHz57PAX5n8frB-9E,562
2
+ conver/__version__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
3
+ conver/_convert.py,sha256=77EqKxxHBmNloK2HzR6zSKI28vxMWACzOJbrUHP1Vpg,7832
4
+ conver/cli.py,sha256=RJ33rmI2uqdYgLH9dKqsJGWc-sO-DKoQ4sGhLdYRAW8,2790
5
+ conver/conver.py,sha256=Ho6RiHVihQsk5AAJafte6fwCCS_5Sf5pyunouNHeMMQ,3780
6
+ conver/scripts/convert.jxa,sha256=7aePzEWbFlPRihPxdgEFfiteyxVxkVhCji4EVq7djNk,7715
7
+ conver/scripts/convert.ps1,sha256=qdm6lfoAUzD3HbyAMprWakQlbGMZW0Ow6KoLbmquOtE,5637
8
+ conver-0.1.0.dist-info/licenses/LICENSE,sha256=EZ2rjk_EQKv3xS71Wwf41DAGJ4gdkeGhM5CGvn6Mf0A,1095
9
+ conver-0.1.0.dist-info/METADATA,sha256=faEL7zeSRKrNj0dMkhYg-r7gxwomeh5_POMPDEFTeIs,7500
10
+ conver-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ conver-0.1.0.dist-info/entry_points.txt,sha256=xILIEXchvJsg2IJFeYlxRDBRxLLPUT--y0Lo1kB_QCo,42
12
+ conver-0.1.0.dist-info/top_level.txt,sha256=_pwrVeQgyeqp-yzROQqRFqK0S0tElmrsb8UXoDJCwiE,7
13
+ conver-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ conver = conver.cli:cli
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+ © 2024 Timur Ulyahin, https://github.com/ucomru
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in
12
+ all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ THE SOFTWARE.
@@ -0,0 +1 @@
1
+ conver