diff --git a/.gitignore b/.gitignore index bb64be8..3f15bde 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,32 @@ +# Office files + +*.doc +*.docx +*.pdf +*.xls +*.xlsx +*.ppt +*.pptx + +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ *.pyc .venv dist/ diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..f522a2a --- /dev/null +++ b/README_CN.md @@ -0,0 +1,83 @@ +# doc2docx + +[](https://pypi.org/project/doc2docx/)https://img.shields.io/pypi/v/doc2docx + +直接在 Windows 或 macOS 上使用 Microsoft Word(**必须已安装**)将 `doc` 文件转换为 `docx` 格式。 + +在 Windows 上,该工具通过 [`win32com`](https://pypi.org/project/pywin32/) 实现;在 macOS 上,则通过 [JXA](https://github.com/JXA-Cookbook/JXA-Cookbook)(JavaScript for Automation,即用 JavaScript 编写的 AppleScript)实现。 + +## 安装 + +通过 brew: + +```bash +brew install cosmojg/tap/doc2docx +``` + + + +通过 [pipx](https://pipxproject.github.io/pipx/): + +```bash +pipx install doc2docx +``` + +通过 pip: + +```bash +pip install doc2docx +``` + + + +## 命令行界面(CLI) + +```context +用法:doc2docx [-h] [--keep-active] [--version] input [output] +示例用法: +原位转换单个 doc 文件,从 myfile.doc 转换为 myfile.docx: + doc2docx myfile.doc +批量转换文件夹内的 doc 文件(原位转换)。输出的 docx 文件将保存在同一文件夹中: + doc2docx myfolder/ +转换单个 doc 文件并指定显式输出文件路径: + doc2docx input.doc output.docx +转换单个 doc 文件并输出到不同的指定文件夹: + doc2docx input.doc output_dir/ +批量转换 doc 文件夹,输出到不同的指定文件夹: + doc2docx input_dir/ output_dir/ +位置参数: + input 输入文件或文件夹。可批量转换整个文件夹或转换单个文件 + output 输出文件或文件夹 +可选参数: + -h, --help 显示此帮助信息并退出 + --keep-active 转换后不关闭 Word + --version 显示版本信息并退出 +``` + + + +## 库调用 + +```python +from doc2docx import convert +convert("input.doc") +convert("input.doc", "output.docx") +convert("my_doc_folder/") +``` + +所有不同的调用方式请参阅上面的 CLI 文档(或 `doc2docx --help`)。命令行和 Python 库的使用方式相同。 + +## Jupyter Notebook + +如果在 Jupyter Notebook 中使用,需要安装 `ipywidgets` 以便正确渲染 tqdm 进度条。 + +```text +pip install ipywidgets +jupyter nbextension enable --py widgetsnbextension +``` + + + +## 致谢 + +衷心感谢 [@AlJohri](https://github.com/AlJohri) 提供的出色项目 [docx2pdf](https://github.com/AlJohri/docx2pdf),本工具正是基于它开发! diff --git a/doc2docx/__init__.py b/doc2docx/__init__.py index 5f08446..b764777 100644 --- a/doc2docx/__init__.py +++ b/doc2docx/__init__.py @@ -3,43 +3,93 @@ import sys from importlib.metadata import version from pathlib import Path +import win32com.client from tqdm.auto import tqdm __version__ = version(__package__) +def convert_one(word_app, input_path, output_path, timeout=30): + """ + Convert a single .doc file to .docx using the Word application. + + Args: + word_app: COM object of Word.Application (already started). + input_path (Path): Path to the source .doc file (must exist). + output_path (Path): Path to the target .docx file (parent directory must exist). + timeout (int): Optional, timeout for opening the document (seconds), default 30 seconds + + Returns: + bool: Returns True if conversion succeeds, False if it fails. + """ + doc = None + try: + # Open the document + doc = word_app.Documents.Open(str(input_path)) + # Save as .docx (wdFormatDocumentDefault = 16) + doc.SaveAs(str(output_path), FileFormat=16) + return True + except Exception as e: + # Can log here, e.g. logging.error(f"Conversion failed {input_path} -> {output_path}: {e}") + return False + finally: + # Ensure the document is closed (if opened) + if doc is not None: + try: + doc.Close(0) # wdDoNotSaveChanges + except Exception: + pass # Ignore close failure to avoid masking the original exception def windows(paths, keep_active): - import win32com.client - + """ + Args: + paths (dict): Must contain keys: + - 'batch' (bool): Whether batch mode + - 'input' (str): Input directory for batch mode, full file path for single file mode + - 'output' (str): Output directory for batch mode, output file path for single file mode (if directory, filename is auto-appended) + keep_active (bool): If False, quit the Word process after conversion. + """ + # Start the Word application word = win32com.client.Dispatch("Word.Application") - wdFormatDocumentDefault = 16 - - if paths["batch"]: - for doc_filepath in tqdm(sorted(Path(paths["input"]).glob("[!~]*.doc*"))): - docx_filepath = Path(paths["output"]) / f"{str(doc_filepath.stem)}.docx" - doc = word.Documents.Open(str(doc_filepath)) - try: - doc.SaveAs(str(docx_filepath), FileFormat=wdFormatDocumentDefault) - except: - raise - finally: - doc.Close(0) - else: - pbar = tqdm(total=1) - doc_filepath = Path(paths["input"]).resolve() - docx_filepath = Path(paths["output"]).resolve() - doc = word.Documents.Open(str(doc_filepath)) - try: - doc.SaveAs(str(docx_filepath), FileFormat=wdFormatDocumentDefault) - except: - raise - finally: - doc.Close(0) - pbar.update(1) - - if not keep_active: - word.Quit() + word.Visible = False # Recommended to run in background + + try: + if paths["batch"]: + # ---- Batch mode ---- + input_dir = Path(paths["input"]) + output_dir = Path(paths["output"]) + output_dir.mkdir(parents=True, exist_ok=True) # Ensure the output directory exists + + # Collect all eligible .doc files (excluding temporary files) + doc_files = list(input_dir.glob("[!~]*.doc")) + # Progress bar + for doc_path in tqdm(doc_files, desc="Batch conversion"): + out_path = output_dir / f"{doc_path.stem}.docx" + success = convert_one(word, doc_path, out_path) + if not success: + # Can log failure info here, or print a warning + print(f"Warning: conversion failed {doc_path.name}") + else: + # ---- Single file mode ---- + in_path = Path(paths["input"]).resolve() + out_path = Path(paths["output"]).resolve() + + # If the output path is a directory, auto-complete the filename + if out_path.is_dir(): + out_path = out_path / f"{in_path.stem}.docx" + + # Single file mode progress bar (displayed only once) + with tqdm(total=1, desc="Converting single file") as pbar: + success = convert_one(word, in_path, out_path) + if success: + pbar.update(1) + else: + print("Conversion failed, please check the log or file validity.") + + finally: + # Regardless of whether an exception occurs, quit Word if not needed to keep active + if not keep_active: + word.Quit() def macos(paths, keep_active): diff --git a/pyproject.toml b/pyproject.toml index 9a196b6..139d86e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [tool.poetry] name = "doc2docx" -version = "0.2.4" +version = "0.2.5" description = "Convert doc to docx on Windows or macOS directly using Microsoft Word (must be installed)." authors = ["Cosmo "] readme = "README.md" packages = [{include = "doc2docx"}] license = "MIT" homepage = "https://github.com/cosmojg/doc2docx" -repository = "https://github.com/cosmojg/doc2docx" +repository = "https://github.com/Harvey-Walker/doc2docx" classifiers = [ "Operating System :: MacOS", "Environment :: MacOS X",