From fcf7aa2ff28dcb98295d41c63fd0959ea3e013e0 Mon Sep 17 00:00:00 2001 From: mulla028 Date: Tue, 29 Oct 2024 22:42:57 -0400 Subject: [PATCH 01/11] Introduced option to command. Udated help text to reflect new option. --- pdfly/cli.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pdfly/cli.py b/pdfly/cli.py index 2353d8d..d8b1b35 100644 --- a/pdfly/cli.py +++ b/pdfly/cli.py @@ -240,5 +240,11 @@ def x2pdf( writable=True, ), ], + format: str = typer.Option( + "A4-portrait", + "--format", + help="Page format for output PDF: Letter, A4-portrait, A4-landscape, or custom dimensions (e.g., 210x297)." + ), ) -> int: - return pdfly.x2pdf.main(x, output) + """Convert one or more files to PDF with the specified page format.""" + return pdfly.x2pdf.main(x, output, format) \ No newline at end of file From 92a2c934ba184c48755594ba845450994144566b Mon Sep 17 00:00:00 2001 From: mulla028 Date: Tue, 29 Oct 2024 22:44:10 -0400 Subject: [PATCH 02/11] Add support for custom page sizes in x2pdf with --format otion handling. --- pdfly/x2pdf.py | 64 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/pdfly/x2pdf.py b/pdfly/x2pdf.py index f5c1283..f914e3c 100644 --- a/pdfly/x2pdf.py +++ b/pdfly/x2pdf.py @@ -1,5 +1,7 @@ """Convert one or more files to PDF. Each file is a page.""" +import re + from pathlib import Path from typing import List @@ -7,6 +9,19 @@ from PIL import Image from rich.console import Console +def get_page_size(format: str): + if format.lower() == 'letter': + return (215.9, 279.4) + elif format.lower() == 'a4-portrait': + return (210, 297) + elif format.lower() == 'a4-landscape': + return (297, 210) + else: + match = re.match(r"(\d+)x(\d+)", format) + if match: + return float(match.group(1)), float(match.group(2)) + else: + raise ValueError(f"Invalid format: {format}") def px_to_mm(px: float) -> float: px_in_inch = 72 @@ -16,39 +31,44 @@ def px_to_mm(px: float) -> float: return mm -def image_to_pdf(pdf: FPDF, x: Path) -> None: +def image_to_pdf(pdf: FPDF, x: Path, page_size: tuple) -> None: cover = Image.open(x) - width: float - height: float width, height = cover.size cover.close() + + # Convert dimensions to millimeters width, height = px_to_mm(width), px_to_mm(height) + page_width, page_height = page_size + + # Scale image to fit page size while maintaining aspect ratio + scale_factor = min(page_width / width, page_height / height) + scaled_width, scaled_height = width * scale_factor, height * scale_factor - pdf.add_page(format=(width, height)) - pdf.image(x, x=0, y=0) + x_offset = (page_width - scaled_width) / 2 + y_offset = (page_height - scaled_height) / 2 + + pdf.add_page(format=page_size) + pdf.image(str(x), x=x_offset, y=y_offset, w=scaled_width, h=scaled_height) -def main(xs: List[Path], output: Path) -> int: + +def main(xs: List[Path], output: Path, format: str = 'A4-portrait') -> int: console = Console() + pdf = FPDF() + + # Retrieve the page size based on format + page_size = get_page_size(format) + for x in xs: path_str = str(x).lower() if path_str.endswith(("doc", "docx", "odt")): - console.print("[red]Error: Cannot convert Word documents to PDF") - return 1 - if not x.exists(): - console.print(f"[red]Error: File '{x}' does not exist.") - return 2 - if output.exists(): - console.print(f"[red]Error: Output file '{output}' exist.") - return 3 - pdf = FPDF( - unit="mm", - ) - for x in xs: - path_str = str(x).lower() + console.print(f"Skipping unsupported file format: {x}", style="yellow") + continue try: - image_to_pdf(pdf, x) - except Exception: - console.print(f"[red]Error: Could not convert '{x}' to a PDF.") + image_to_pdf(pdf, x, page_size) + except Exception as e: + console.print(f"Error processing {x}: {e}", style="red") + pdf.output(str(output)) + console.print(f"PDF created successfully at {output}", style="green") return 0 From 7eab9757363293e76d0571c4aa52ff8db7eb904f Mon Sep 17 00:00:00 2001 From: mulla028 Date: Tue, 29 Oct 2024 22:45:08 -0400 Subject: [PATCH 03/11] Add test for --format option in x2pdf to validate custom page sizes. --- tests/test_x2pdf.py | 51 +++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/tests/test_x2pdf.py b/tests/test_x2pdf.py index aa74f37..7a344ae 100644 --- a/tests/test_x2pdf.py +++ b/tests/test_x2pdf.py @@ -9,23 +9,42 @@ from .conftest import run_cli -def test_x2pdf(capsys, tmp_path: Path) -> None: +def test_x2pdf_with_format(capsys, tmp_path: Path) -> None: # Arrange output = tmp_path / "out.pdf" assert not output.exists() + + formats_to_test = [ + "Letter", + "A4-portrait", + "A4-landscape", + "210x297", + "invalid-format" + ] + + for format_option in formats_to_test: + # Act + exit_code = run_cli( + [ + "x2pdf", + "sample-files/003-pdflatex-image/page-0-Im1.jpg", + "--output", + str(output), + "--format", + format_option, + ] + ) - # Act - exit_code = run_cli( - [ - "x2pdf", - "sample-files/003-pdflatex-image/page-0-Im1.jpg", - "--output", - str(output), - ] - ) - - # Assert - captured = capsys.readouterr() - assert exit_code == 0, captured - assert captured.out == "" - assert output.exists() + # Assert + captured = capsys.readouterr() + + # For valid formats, we expect a successful exit code and the output file to exist + if format_option != "invalid-format": + assert exit_code == 0, captured + assert captured.out == "" + assert output.exists() + else: + # For an invalid format, we expect a non-zero exit code (indicating failure) + assert exit_code != 0 + assert "Invalid format" in captured.err # Check for expected error message + output.unlink(missing_ok=True) # Clean up for the next test iteration From 3108e70bb0947dae7d9a87f9aeecb7b6c37591d7 Mon Sep 17 00:00:00 2001 From: mulla028 Date: Wed, 30 Oct 2024 13:42:46 -0400 Subject: [PATCH 04/11] add optional --format parameter to x2pdf command for custom page sizes. --- pdfly/cli.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pdfly/cli.py b/pdfly/cli.py index d8b1b35..a581688 100644 --- a/pdfly/cli.py +++ b/pdfly/cli.py @@ -227,8 +227,7 @@ def compress( ) -> None: pdfly.compress.main(pdf, output) - -@entry_point.command(name="x2pdf", help=pdfly.x2pdf.__doc__) # type: ignore[misc] +@entry_point.command() def x2pdf( x: List[Path], output: Annotated[ @@ -241,10 +240,9 @@ def x2pdf( ), ], format: str = typer.Option( - "A4-portrait", + None, "--format", - help="Page format for output PDF: Letter, A4-portrait, A4-landscape, or custom dimensions (e.g., 210x297)." + help="Optional page format for output PDF: Letter, A4-portrait, A4-landscape, or custom dimensions (e.g., 210x297). If omitted, no format is enforced." ), ) -> int: - """Convert one or more files to PDF with the specified page format.""" return pdfly.x2pdf.main(x, output, format) \ No newline at end of file From aca52060fccdb3f939b476a3ced5d7ecc2d27617 Mon Sep 17 00:00:00 2001 From: mulla028 Date: Wed, 30 Oct 2024 13:43:19 -0400 Subject: [PATCH 05/11] implement get_page_size and enhanced error handling for format options. --- pdfly/x2pdf.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/pdfly/x2pdf.py b/pdfly/x2pdf.py index f914e3c..b251a09 100644 --- a/pdfly/x2pdf.py +++ b/pdfly/x2pdf.py @@ -10,18 +10,19 @@ from rich.console import Console def get_page_size(format: str): - if format.lower() == 'letter': - return (215.9, 279.4) - elif format.lower() == 'a4-portrait': - return (210, 297) - elif format.lower() == 'a4-landscape': - return (297, 210) - else: - match = re.match(r"(\d+)x(\d+)", format) - if match: - return float(match.group(1)), float(match.group(2)) - else: - raise ValueError(f"Invalid format: {format}") + """Get page dimensions based on format.""" + sizes = { + "A4": (210, 297), "A3": (297, 420), "A2": (420, 594), + "A1": (594, 841), "A0": (841, 1189), "Letter": (215.9, 279.4), + "Legal": (215.9, 355.6) + } + match = re.match(r"(A\d|B\d|C\d|Letter|Legal)(-landscape)?$", format, re.IGNORECASE) + if match: + size_key = match.group(1).upper() + if size_key in sizes: + width, height = sizes[size_key] + return (height, width) if match.group(2) else (width, height) + raise ValueError(f"Invalid or unsupported page format provided: {format}") def px_to_mm(px: float) -> float: px_in_inch = 72 @@ -52,13 +53,11 @@ def image_to_pdf(pdf: FPDF, x: Path, page_size: tuple) -> None: -def main(xs: List[Path], output: Path, format: str = 'A4-portrait') -> int: +def main(xs: List[Path], output: Path, format: str = None) -> int: + """Main function to generate PDF with images fitted to specified page format.""" console = Console() - pdf = FPDF() - - # Retrieve the page size based on format - page_size = get_page_size(format) - + pdf = FPDF(unit="mm") + page_size = get_page_size(format) if format else None for x in xs: path_str = str(x).lower() if path_str.endswith(("doc", "docx", "odt")): @@ -68,7 +67,7 @@ def main(xs: List[Path], output: Path, format: str = 'A4-portrait') -> int: image_to_pdf(pdf, x, page_size) except Exception as e: console.print(f"Error processing {x}: {e}", style="red") - + return 1 pdf.output(str(output)) console.print(f"PDF created successfully at {output}", style="green") return 0 From bf50183fbfe13a66ce474ca8293d27d4b40ec2d8 Mon Sep 17 00:00:00 2001 From: mulla028 Date: Wed, 30 Oct 2024 13:46:01 -0400 Subject: [PATCH 06/11] :: --- pdfly/up2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pdfly/up2.py b/pdfly/up2.py index 01b1c2a..77dae48 100644 --- a/pdfly/up2.py +++ b/pdfly/up2.py @@ -27,3 +27,4 @@ def main(pdf: Path, output: Path) -> None: with open(output, "wb") as fp: writer.write(fp) print("done.") + \ No newline at end of file From 9fcb11a86c0b750ba618b16f9dfaac208141298d Mon Sep 17 00:00:00 2001 From: mulla028 Date: Wed, 30 Oct 2024 14:07:42 -0400 Subject: [PATCH 07/11] test_x2pdf.py returned to the previous state --- tests/test_x2pdf.py | 51 ++++++++++++++------------------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/tests/test_x2pdf.py b/tests/test_x2pdf.py index 7a344ae..0938bf4 100644 --- a/tests/test_x2pdf.py +++ b/tests/test_x2pdf.py @@ -9,42 +9,23 @@ from .conftest import run_cli -def test_x2pdf_with_format(capsys, tmp_path: Path) -> None: +def test_x2pdf(capsys, tmp_path: Path) -> None: # Arrange output = tmp_path / "out.pdf" assert not output.exists() - - formats_to_test = [ - "Letter", - "A4-portrait", - "A4-landscape", - "210x297", - "invalid-format" - ] - - for format_option in formats_to_test: - # Act - exit_code = run_cli( - [ - "x2pdf", - "sample-files/003-pdflatex-image/page-0-Im1.jpg", - "--output", - str(output), - "--format", - format_option, - ] - ) - # Assert - captured = capsys.readouterr() - - # For valid formats, we expect a successful exit code and the output file to exist - if format_option != "invalid-format": - assert exit_code == 0, captured - assert captured.out == "" - assert output.exists() - else: - # For an invalid format, we expect a non-zero exit code (indicating failure) - assert exit_code != 0 - assert "Invalid format" in captured.err # Check for expected error message - output.unlink(missing_ok=True) # Clean up for the next test iteration + # Act + exit_code = run_cli( + [ + "x2pdf", + "sample-files/003-pdflatex-image/page-0-Im1.jpg", + "--output", + str(output), + ] + ) + + # Assert + captured = capsys.readouterr() + assert exit_code == 0, captured + assert captured.out == "" + assert output.exists() \ No newline at end of file From 5d7d5abe9c75f54e546d404aeddc37574a6d93fc Mon Sep 17 00:00:00 2001 From: mulla028 Date: Thu, 31 Oct 2024 12:45:25 -0400 Subject: [PATCH 08/11] Returned back entry point command parameters, as somewho it's been deleted --- pdfly/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdfly/cli.py b/pdfly/cli.py index a581688..745d1f1 100644 --- a/pdfly/cli.py +++ b/pdfly/cli.py @@ -227,7 +227,7 @@ def compress( ) -> None: pdfly.compress.main(pdf, output) -@entry_point.command() +@entry_point.command(name="x2pdf", help=pdfly.x2pdf.__doc__) def x2pdf( x: List[Path], output: Annotated[ From 0cb1d6e70b046d8c5fc4b818df801d9b5f0ebd19 Mon Sep 17 00:00:00 2001 From: mulla028 Date: Thu, 31 Oct 2024 12:49:39 -0400 Subject: [PATCH 09/11] Test unit returned as a separate command --- tests/test_x2pdf.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/test_x2pdf.py b/tests/test_x2pdf.py index 0938bf4..8cfdc9a 100644 --- a/tests/test_x2pdf.py +++ b/tests/test_x2pdf.py @@ -28,4 +28,44 @@ def test_x2pdf(capsys, tmp_path: Path) -> None: captured = capsys.readouterr() assert exit_code == 0, captured assert captured.out == "" - assert output.exists() \ No newline at end of file + assert output.exists() + +def test_x2pdf_with_format(capsys, tmp_path: Path) -> None: + # Arrange + output = tmp_path / "out.pdf" + assert not output.exists() + + formats_to_test = [ + "Letter", + "A4-portrait", + "A4-landscape", + "210x297", + "invalid-format" + ] + + for format_option in formats_to_test: + # Act + exit_code = run_cli( + [ + "x2pdf", + "sample-files/003-pdflatex-image/page-0-Im1.jpg", + "--output", + str(output), + "--format", + format_option, + ] + ) + + # Assert + captured = capsys.readouterr() + + # For valid formats, we expect a successful exit code and the output file to exist + if format_option != "invalid-format": + assert exit_code == 0, captured + assert captured.out == "" + assert output.exists() + else: + # For an invalid format, we expect a non-zero exit code (indicating failure) + assert exit_code != 0 + assert "Invalid format" in captured.err # Check for expected error message + output.unlink(missing_ok=True) # Clean up for the next test iteration \ No newline at end of file From ecda6030f37643c58e5deeb4f61d60adcdb7c759 Mon Sep 17 00:00:00 2001 From: mulla028 Date: Thu, 31 Oct 2024 13:02:04 -0400 Subject: [PATCH 10/11] Update x2pdf main function to handle errors appropriately, returning non-zero code and adjusting success message if issue occur. --- pdfly/x2pdf.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pdfly/x2pdf.py b/pdfly/x2pdf.py index b251a09..4edb53d 100644 --- a/pdfly/x2pdf.py +++ b/pdfly/x2pdf.py @@ -58,6 +58,9 @@ def main(xs: List[Path], output: Path, format: str = None) -> int: console = Console() pdf = FPDF(unit="mm") page_size = get_page_size(format) if format else None + + error_occurred = False # Flag to track if any errors happen + for x in xs: path_str = str(x).lower() if path_str.endswith(("doc", "docx", "odt")): @@ -67,7 +70,13 @@ def main(xs: List[Path], output: Path, format: str = None) -> int: image_to_pdf(pdf, x, page_size) except Exception as e: console.print(f"Error processing {x}: {e}", style="red") - return 1 + error_occurred = True + pdf.output(str(output)) - console.print(f"PDF created successfully at {output}", style="green") - return 0 + + if error_occurred: + console.print(f"PDF created at {output}, but some files encountered errors.", style="yellow") + return 1 + else: + console.print(f"PDF created successfully at {output}", style="green") + return 0 From c724c8483030c8bcbc44a791d40d1d923d2ec9bc Mon Sep 17 00:00:00 2001 From: mulla028 Date: Thu, 31 Oct 2024 13:04:29 -0400 Subject: [PATCH 11/11] Enhance get_page_size to support -portrait suffix in page format handling. --- pdfly/x2pdf.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pdfly/x2pdf.py b/pdfly/x2pdf.py index 4edb53d..b42f8c8 100644 --- a/pdfly/x2pdf.py +++ b/pdfly/x2pdf.py @@ -16,14 +16,21 @@ def get_page_size(format: str): "A1": (594, 841), "A0": (841, 1189), "Letter": (215.9, 279.4), "Legal": (215.9, 355.6) } - match = re.match(r"(A\d|B\d|C\d|Letter|Legal)(-landscape)?$", format, re.IGNORECASE) + match = re.match(r"(A\d|B\d|C\d|Letter|Legal)(-(landscape|portrait))?$", format, re.IGNORECASE) if match: size_key = match.group(1).upper() if size_key in sizes: width, height = sizes[size_key] - return (height, width) if match.group(2) else (width, height) + orientation = match.group(3) + if orientation == "landscape": + return (height, width) + elif orientation == "portrait": + return (width, height) + else: + return (width, height) raise ValueError(f"Invalid or unsupported page format provided: {format}") + def px_to_mm(px: float) -> float: px_in_inch = 72 mm_in_inch = 25.4