Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion pdfly/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,5 +240,11 @@ def x2pdf(
writable=True,
),
],
format: str = typer.Option(
"A4-portrait",
Comment thread
Lucas-C marked this conversation as resolved.
Outdated
"--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)
64 changes: 42 additions & 22 deletions pdfly/x2pdf.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
"""Convert one or more files to PDF. Each file is a page."""

import re

from pathlib import Path
from typing import List

from fpdf import FPDF
from PIL import Image
from rich.console import Console

def get_page_size(format: str):
Comment thread
Lucas-C marked this conversation as resolved.
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}")
Comment thread
Lucas-C marked this conversation as resolved.
Outdated

def px_to_mm(px: float) -> float:
px_in_inch = 72
Expand All @@ -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()
Comment thread
Lucas-C marked this conversation as resolved.
Outdated

# 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
Comment thread
Lucas-C marked this conversation as resolved.
Outdated
51 changes: 35 additions & 16 deletions tests/test_x2pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,42 @@
from .conftest import run_cli


def test_x2pdf(capsys, tmp_path: Path) -> None:
Comment thread
Lucas-C marked this conversation as resolved.
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",
Comment thread
Lucas-C marked this conversation as resolved.
"--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()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be interesting to also validate the resulting pages dimensions.

This can be checked with pdfly pagemeta $pdf_filepath $page_index

Or using the underlying pypdf library: PdfReader(pdf: Path).mediabox/.cropbox/.artbox/.bleedbox

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you did not handle that feedback comment @mulla028 🙂

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