Skip to content

Commit

Permalink
Merge pull request #36 from pmaher86/weasyprint
Browse files Browse the repository at this point in the history
move to weasyprint
  • Loading branch information
pmaher86 authored Oct 7, 2024
2 parents cb3961f + 22ac3dd commit 31884d0
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 77 deletions.
7 changes: 1 addition & 6 deletions .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,8 @@ jobs:
pip install --upgrade pip
pip install -r requirements.txt
- name: Install wkhtmltopdf
run: |
sudo apt-get update
sudo apt-get -y install wkhtmltopdf
- name: Install package
run: pip install .[dev]
run: pip install .[dev,pdf]

- name: Run pre-commit
run: pre-commit run --show-diff-on-failure --color=always --all-files
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Blacksquare
![Build Status](https://github.com/pmaher86/blacksquare/actions/workflows/build-and-test.yaml/badge.svg) ![Documentation Status](https://readthedocs.org/projects/blacksquare/badge/?version=latest)

Blacksquare is a Python package for crossword creators. It aims to be an intuitive interface for working with crossword puzzles programmatically. It also has tools for finding valid fills, and HTML rendering that plugs nicely into Jupyter notebooks. Blacksquare supports import and export from the .puz format via [puzpy](https://github.com/alexdej/puzpy), as well as .pdf export in the [New York Times submission format](https://www.nytimes.com/puzzles/submissions/crossword) (requires [wkhtmltopdf](https://wkhtmltopdf.org/)).
Blacksquare is a Python package for crossword creators. It aims to be an intuitive interface for working with crossword puzzles programmatically. It also has tools for finding valid fills, and HTML rendering that plugs nicely into Jupyter notebooks. Blacksquare supports import and export from the .puz format via [puzpy](https://github.com/alexdej/puzpy), as well as .pdf export in the [New York Times submission format](https://www.nytimes.com/puzzles/submissions/crossword) (requires the [pdf] extra).

## Native HTML rendering in Jupyter
![Jupyter example](assets/jupyter.png?raw=true)
Expand Down Expand Up @@ -173,7 +173,9 @@ There's clearly some extra curation that could be done to improve the word list,
## Installation
`pip install blacksquare`

You'll also need to install [wkhtmltopdf](https://wkhtmltopdf.org/) for .pdf export to work.
or if you want to enable pdf export

`pip install blacksquare[pdf]`

## Future plans
Blacksquare is not a GUI application and isn't intended to be one. Blacksquare is also not a package for solving crossword puzzles.
Expand Down
11 changes: 2 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,7 @@ name = "blacksquare"
description = "A package for creating crossword puzzles"
authors = [{ name = "Patrick Maher", email = "pmaher86@gmail.com" }]
readme = "README.md"
dependencies = [
"networkx",
"numpy",
"pandas",
"pdfkit",
"puzpy",
"pypdf",
"rich",
]
dependencies = ["networkx", "numpy", "pandas", "puzpy", "rich"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
Expand All @@ -28,6 +20,7 @@ Source = "https://github.com/pmaher86/blacksquare"

[project.optional-dependencies]
dev = ["pre-commit", "pytest", "uv"]
pdf = ["pypdf", "weasyprint"]

[tool.setuptools_scm]
local_scheme = "no-local-version"
Expand Down
39 changes: 35 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --extra=dev pyproject.toml -o requirements.txt
# uv pip compile --extra dev --extra pdf pyproject.toml -o requirements.txt
brotli==1.1.0
# via fonttools
cffi==1.17.1
# via weasyprint
cfgv==3.4.0
# via pre-commit
cssselect2==0.7.0
# via weasyprint
distlib==0.3.8
# via virtualenv
filelock==3.16.1
# via virtualenv
fonttools==4.54.1
# via weasyprint
html5lib==1.1
# via weasyprint
identify==2.6.1
# via pre-commit
iniconfig==2.0.0
Expand All @@ -26,8 +36,8 @@ packaging==23.1
# via pytest
pandas==2.1.1
# via blacksquare (pyproject.toml)
pdfkit==1.0.0
# via blacksquare (pyproject.toml)
pillow==10.4.0
# via weasyprint
platformdirs==4.3.6
# via virtualenv
pluggy==1.3.0
Expand All @@ -36,10 +46,16 @@ pre-commit==3.8.0
# via blacksquare (pyproject.toml)
puzpy==0.2.5
# via blacksquare (pyproject.toml)
pycparser==2.22
# via cffi
pydyf==0.11.0
# via weasyprint
pygments==2.16.1
# via rich
pypdf==3.16.2
# via blacksquare (pyproject.toml)
pyphen==0.16.0
# via weasyprint
pytest==7.4.2
# via blacksquare (pyproject.toml)
python-dateutil==2.8.2
Expand All @@ -51,10 +67,25 @@ pyyaml==6.0.2
rich==13.5.3
# via blacksquare (pyproject.toml)
six==1.16.0
# via python-dateutil
# via
# html5lib
# python-dateutil
tinycss2==1.3.0
# via
# cssselect2
# weasyprint
tzdata==2023.3
# via pandas
uv==0.4.18
# via blacksquare (pyproject.toml)
virtualenv==20.26.6
# via pre-commit
weasyprint==62.3
# via blacksquare (pyproject.toml)
webencodings==0.5.1
# via
# cssselect2
# html5lib
# tinycss2
zopfli==0.2.3
# via fonttools
128 changes: 72 additions & 56 deletions src/blacksquare/crossword.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@

import networkx as nx
import numpy as np
import pdfkit
import puz
import rich.box
from pypdf import PdfReader, PdfWriter
from rich.console import Console
from rich.live import Live
from rich.table import Table

from blacksquare.cell import Cell
from blacksquare.html import CSS_TEMPLATE
from blacksquare.symmetry import Symmetry
from blacksquare.types import (
CellIndex,
Expand All @@ -29,6 +28,13 @@
from blacksquare.word import Word
from blacksquare.word_list import DEFAULT_WORDLIST, WordList

try:
import pypdf
import weasyprint
except ImportError:
weasyprint = None
pypdf = None

BLACK, EMPTY = SpecialCellValue.BLACK, SpecialCellValue.EMPTY
ACROSS, DOWN = Direction.ACROSS, Direction.DOWN

Expand Down Expand Up @@ -203,15 +209,36 @@ def to_pdf(
name, address, etc.). Each list element will be one line in the header.
"""

if weasyprint is None:
raise ImportError(
"Can't import weasyprint, run pip install blacksquare[pdf] to install."
)

header_html = "<br />".join(header) if header else ""
grid_html = f"""
<html>
<head><meta charset="utf-8"></head>
<head><meta charset="utf-8">
<style>
@page {{
margin:0.25 in;
margin-bottom: 0;
}}
@media print {{
div {{
break-inside: avoid-page !important;
}}
}}
</style>
</head>
<body>
<div style='font-size:18pt;'>
<div style='font-size:14pt; break-after: avoid-page !important;'>
{header_html}
</div>
<div style='position:absolute;left:50%;top:50%;transform: translate(-50%, -50%);'>
<br /> <br /> <br /> <br />
<div style='margin: auto;'>
{self._grid_html(size_px=600)}
</div>
</body></html>
Expand All @@ -235,7 +262,7 @@ def clue_rows(direction):
table {{
text-align:left;
width:100%;
font-size:18pt;
font-size:16pt;
border-spacing:1rem;
}}
</style>
Expand All @@ -250,21 +277,10 @@ def clue_rows(direction):
</tbody></table>
</body></html>
"""
merger = PdfWriter()
merger = pypdf.PdfWriter()
for html_page in [grid_html, clue_html]:
pdf = pdfkit.from_string(
html_page,
False,
options={
"quiet": None,
"margin-top": "0.5in",
"margin-right": "0.5in",
"margin-bottom": "0.5in",
"margin-left": "0.5in",
"encoding": "UTF-8",
},
)
merger.append(PdfReader(io.BytesIO(pdf)))
pdf = weasyprint.HTML(string=html_page, encoding="UTF-8").write_pdf()
merger.append(pypdf.PdfReader(io.BytesIO(pdf)))
merger.write(str(filename))
merger.close()

Expand Down Expand Up @@ -761,45 +777,45 @@ def _grid_html(self, size_px: Optional[int] = None) -> str:
size_px = size_px or self.display_size_px
# Random suffix is a hack to ensure correct display in Jupyter settings
suffix = token_hex(4)
circle_string = f"<div class='circle{suffix}'> </div>"
row_elems = []
for r in range(self._num_rows):
cells = []
for c in range(self._num_cols):
number = self._numbers[r, c]
cells.append(
f"""<td class='xw{suffix}{f" black{suffix}" if self._grid[r, c] == BLACK else ""}{f" gray{suffix}" if self._grid[r, c].shaded else ""}'>
<div class='number{suffix}'> {number if number else ""}</div>
<div class='value{suffix}'>
{self._grid[r,c].str if self._grid[r,c] != BLACK else ""}
</div>
{circle_string if self._grid[r,c].circled else ""}
</td >"""
)
row_elems.append(f"<tr class='xw{suffix}'>{''.join(cells)}</tr>")
cells = []
for c in self.itercells():
c.number
cell_number_span = f'<span class="cell-number">{c.number or ""}</span>'
letter_span = f'<span class="letter">{c.value if c!=BLACK else ""}</span>'
circle_span = '<span class="circle"></span>'
if c == BLACK:
extra_class = " black"
elif c.shaded:
extra_class = " gray"
else:
extra_class = ""
cell_div = f"""
<div class="crossword-cell{suffix}{extra_class}">
{cell_number_span}
{letter_span}
{circle_span if c.circled else ""}
</div>
"""
cells.append(cell_div)
aspect_ratio = self.num_rows / self.num_cols
cell_size = size_px / max(self.num_rows, self.num_cols)
return """
<div>
<style scoped>
table.xw{suffix} {{table-layout:fixed; background-color:white;width:{width}px;height:{height}px;}}
td.xw{suffix} {{outline: 2px solid black;outline-offset: -1px;position: relative;font-family: Arial, Helvetica, sans-serif;}}
tr.xw{suffix} {{background-color: white !important;}}
.number{suffix} {{position: absolute;top: 2px;left: 2px;font-size: {num_font}px;font-weight: normal;user-select: none; color: black;}}
.value{suffix} {{position: absolute;bottom:0;left: 50%;font-weight: bold;font-size: {val_font}px; transform: translate(-50%, 0%); color: black;}}
.black{suffix} {{background-color: black;}}
.gray{suffix} {{background-color: lightgrey;}}
.circle{suffix} {{position: absolute; border-radius: 50%; border: 1px solid black; right: 0px; left: 0px; top: 0px; bottom: 0px;}}
</style>
<table class='xw{suffix}'><tbody>
{rows}
</tbody</table>
</div>
""".format(
css = CSS_TEMPLATE.format(
num_cols=self.num_cols,
height=size_px * min(1, aspect_ratio),
width=size_px * min(1, 1 / aspect_ratio),
rows="\n".join(row_elems),
num_font_size=int(cell_size * 0.3),
val_font_size=int(cell_size * 0.6),
circle_dim=cell_size - 1,
suffix=suffix,
num_font=int(cell_size * 0.3),
val_font=int(cell_size * 0.6),
)
cells_html = "\n".join(cells)
return f"""
<div>
<style scoped>
{css}
</style>
<div class="crossword{suffix}">
{cells_html}
</div>
</div>
"""
54 changes: 54 additions & 0 deletions src/blacksquare/html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
CSS_TEMPLATE = """
.crossword{suffix} {{
display: grid;
grid-template-columns: repeat({num_cols}, 1fr);
grid-auto-rows: 1fr;
gap: 0px;
width: {width}px;
height: {height}px;
}}
.crossword-cell{suffix} {{
position: relative;
background-color: white;
outline: 1px solid black;
font-family: Arial, Helvetica, sans-serif;
aspect-ratio: 1;
}}
.crossword-cell{suffix} .cell-number {{
position: absolute;
top: 2px;
left: 2px;
font-size: {num_font_size}px;
color: black;
user-select: none;
}}
.crossword-cell{suffix} .letter {{
font-size: {val_font_size}px;
position: absolute;
bottom: 0;
left: 50%;
transform: translate(-50%, 0%);
color: black;
}}
.black {{
background-color: black;
outline: 1px solid gray;
}}
.gray {{
background-color: lightgray;
}}
.crossword-cell{suffix} .circle {{
position: absolute;
border-radius: 50%;
border: 1px solid black;
height: {circle_dim}px;
width: {circle_dim}px;
margin: -1px;
}}
"""

0 comments on commit 31884d0

Please sign in to comment.