clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

generative artwork pdf cover

Dami committed d2b8695
commit
d2b869533fbea5777c4120c444523fcdd9a3ba31
parent
1cf1738c0f610104fcf382767dd84d15d4420777
author
Dami <dami.etoile@gmail.com>
12 files changed, +1325 -1019
Mcmd/export_pdf.go3+
Mcmd/export_pdf_test.go6+
Dnonogram-pack-01.md776----------------------------
Apdfexport/cover_art.go841++++++++++++++++++++++++++++++
Apdfexport/cover_art_test.go89+++
Mpdfexport/jsonl.go14+-
Mpdfexport/jsonl_test.go98+++
Mpdfexport/printdata.go130+++++
Mpdfexport/printdata_test.go51++
Mpdfexport/render.go123+++--
Mpdfexport/render_cover.go212+-------
Mpdfexport/types.go1+
M cmd/export_pdf.go +3 -0
M cmd/export_pdf_test.go +6 -0
D nonogram-pack-01.md +0 -776
@@ -1,776 +0,0 @@
1-# PuzzleTea Export2-3-- Generated: 2026-02-21T20:42:05-07:004-- Version: v1.6.0-1-gd260f2e-dirty5-- Category: Nonogram6-- Mode Selection: Standard7-- Count: 318-- Seed: meow9-10-## ember-newt - 111-12-### Puzzle Grid with Integrated Hints13-14-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |15-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |16-| . | . | . | . | 6 | 4 | 2 | 1 | . | . | . | 1 | 2 | . |17-| . | . | . | . | 1 | 1 | 4 | 5 | 2 | 5 | 5 | 2 | 5 | 2 |18-| . | . | . | . | 1 | 1 | 1 | 2 | 4 | 2 | 3 | 3 | 1 | 7 |19-| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |20-| . | . | 3 | 6 | . | . | . | . | . | . | . | . | . | . |21-| . | . | . | 7 | . | . | . | . | . | . | . | . | . | . |22-| . | 2 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |23-| . | . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |24-| . | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |25-| . | 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |26-| . | . | 3 | 6 | . | . | . | . | . | . | . | . | . | . |27-| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |28-| . | . | 4 | 3 | . | . | . | . | . | . | . | . | . | . |29-30-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.31-32----33-34-## ember-moss - 235-36-### Puzzle Grid with Integrated Hints37-38-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |39-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |40-| . | . | . | . | . | 1 | . | . | . | . | 2 | . | . | 2 |41-| . | . | . | . | . | 4 | . | . | . | . | 1 | . | . | 2 |42-| . | . | . | . | 1 | 1 | 3 | 6 | 3 | 3 | 1 | 2 | 1 | 1 |43-| . | . | . | . | 5 | 1 | 2 | 3 | 3 | 6 | 2 | 5 | 8 | 1 |44-| . | 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |45-| . | 1 | 6 | 1 | . | . | . | . | . | . | . | . | . | . |46-| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |47-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |48-| 2 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |49-| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |50-| . | 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |51-| . | 2 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |52-| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |53-| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |54-55-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.56-57----58-59-## stone-viper - 360-61-### Puzzle Grid with Integrated Hints62-63-| R1 | R2 | R3 | R4 | R5 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |64-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |65-| . | . | . | . | . | 2 | 2 | . | . | 4 | . | 1 | 1 | 2 | 2 |66-| . | . | . | . | . | 3 | 1 | 3 | 1 | 1 | 2 | 3 | 1 | 1 | 1 |67-| . | . | . | . | . | 2 | 2 | 5 | 3 | 2 | 3 | 3 | 1 | 4 | 2 |68-| . | . | . | 1 | 1 | . | . | . | . | . | . | . | . | . | . |69-| 1 | 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |70-| . | . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |71-| . | . | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |72-| . | . | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |73-| . | 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |74-| . | . | . | 6 | 1 | . | . | . | . | . | . | . | . | . | . |75-| . | . | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |76-| . | . | 3 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |77-| . | 3 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |78-79-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.80-81----82-83-## lonely-vale - 484-85-### Puzzle Grid with Integrated Hints86-87-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |88-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |89-| . | . | . | . | . | . | 1 | . | . | . | . | . | . |90-| . | . | . | . | . | . | 4 | . | . | 3 | 2 | . | . |91-| . | . | . | 6 | 3 | 2 | 1 | . | 5 | 3 | 3 | 5 | 6 |92-| . | . | . | 3 | 6 | 4 | 1 | 9 | 3 | 1 | 1 | 1 | 2 |93-| . | 7 | 1 | . | . | . | . | . | . | . | . | . | . |94-| 3 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |95-| . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |96-| 1 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |97-| . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |98-| . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |99-| . | 2 | 5 | . | . | . | . | . | . | . | . | . | . |100-| 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |101-| 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |102-| 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |103-104-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.105-106----107-108-## arctic-basalt - 5109-110-### Puzzle Grid with Integrated Hints111-112-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |113-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |114-| . | . | . | . | . | 1 | . | . | . | . | . | . | . | . |115-| . | . | . | . | . | 1 | . | 1 | . | . | . | . | . | . |116-| . | . | . | . | 6 | 1 | 5 | 1 | 8 | 2 | 4 | 2 | 3 | 4 |117-| . | . | . | . | 1 | 4 | 3 | 2 | 1 | 3 | 4 | 5 | 2 | 2 |118-| 3 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |119-| 1 | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |120-| 3 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |121-| 1 | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |122-| . | . | 6 | 1 | . | . | . | . | . | . | . | . | . | . |123-| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |124-| . | . | 2 | 5 | . | . | . | . | . | . | . | . | . | . |125-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |126-| . | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |127-| . | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |128-129-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.130-131----132-133-## calm-cloud - 6134-135-### Puzzle Grid with Integrated Hints136-137-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |138-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |139-| . | . | . | 5 | 3 | 5 | . | . | 2 | 3 | 4 | . | 3 |140-| . | . | . | 1 | 1 | 1 | 5 | 1 | 2 | 3 | 3 | 4 | 2 |141-| . | . | . | 2 | 2 | 1 | 3 | 8 | 4 | 1 | 1 | 5 | 1 |142-| . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |143-| . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |144-| . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |145-| . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |146-| 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |147-| 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |148-| 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |149-| . | 1 | 6 | . | . | . | . | . | . | . | . | . | . |150-| . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |151-| . | 1 | 6 | . | . | . | . | . | . | . | . | . | . |152-153-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.154-155----156-157-## scarlet-sage - 7158-159-### Puzzle Grid with Integrated Hints160-161-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |162-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |163-| . | . | . | . | . | . | . | . | . | 1 | . | . | . | 1 |164-| . | . | . | . | . | . | 2 | . | 5 | 1 | . | 1 | 1 | 1 |165-| . | . | . | . | 8 | 4 | 1 | 4 | 1 | 1 | . | 2 | 2 | 2 |166-| . | . | . | . | 1 | 2 | 1 | 3 | 2 | 3 | 8 | 3 | 1 | 1 |167-| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |168-| 1 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |169-| . | . | . | 8 | . | . | . | . | . | . | . | . | . | . |170-| 2 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |171-| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |172-| . | 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |173-| . | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |174-| 2 | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |175-| . | . | . | 6 | . | . | . | . | . | . | . | . | . | . |176-| . | 1 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |177-178-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.179-180----181-182-## wandering-viper - 8183-184-### Puzzle Grid with Integrated Hints185-186-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |187-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |188-| . | . | . | . | . | . | . | . | . | . | 2 | . | 1 | . |189-| . | . | . | . | . | 5 | . | 2 | 1 | . | 2 | . | 2 | 3 |190-| . | . | . | . | 4 | 1 | 5 | 4 | 1 | 2 | 1 | 1 | 1 | 2 |191-| . | . | . | . | 4 | 1 | 3 | 2 | 4 | 6 | 2 | 6 | 2 | 1 |192-| . | . | 1 | 4 | . | . | . | . | . | . | . | . | . | . |193-| . | . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |194-| . | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |195-| . | 4 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |196-| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |197-| 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |198-| . | . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |199-| 1 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |200-| . | . | 1 | 8 | . | . | . | . | . | . | . | . | . | . |201-| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |202-203-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.204-205----206-207-## twilight-owl - 9208-209-### Puzzle Grid with Integrated Hints210-211-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |212-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |213-| . | . | . | . | 3 | . | . | . | 3 | 2 | . | 4 | . | . |214-| . | . | . | . | 1 | 6 | 7 | . | 1 | 1 | . | 2 | 1 | 2 |215-| . | . | . | . | 1 | 1 | 1 | 6 | 2 | 1 | 5 | 2 | 6 | 2 |216-| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |217-| 3 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |218-| . | 3 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |219-| . | 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |220-| . | 4 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |221-| . | . | 4 | 3 | . | . | . | . | . | . | . | . | . | . |222-| . | . | 2 | 4 | . | . | . | . | . | . | . | . | . | . |223-| . | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |224-| 1 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |225-| . | 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |226-227-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.228-229----230-231-## silver-bison - 10232-233-### Puzzle Grid with Integrated Hints234-235-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |236-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |237-| . | . | . | . | . | . | . | . | . | . | . | . | 1 |238-| . | . | . | . | 1 | 2 | . | . | . | 2 | . | . | 2 |239-| . | . | . | 5 | 3 | 1 | . | . | . | 1 | 2 | . | 1 |240-| . | . | . | 2 | 1 | 1 | 3 | 2 | 8 | 1 | 2 | 3 | 1 |241-| . | . | . | 1 | 1 | 1 | 6 | 6 | 1 | 2 | 3 | 6 | 1 |242-| . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |243-| . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |244-| 4 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |245-| 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |246-| . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |247-| . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |248-| 1 | 5 | 1 | . | . | . | . | . | . | . | . | . | . |249-| 2 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |250-| . | 2 | 3 | . | . | . | . | . | . | . | . | . | . |251-| . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |252-253-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.254-255----256-257-## pearl-river - 11258-259-### Puzzle Grid with Integrated Hints260-261-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |262-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |263-| . | . | . | . | 1 | . | 1 | 1 | . | . | 3 | . | . | . |264-| . | . | . | . | 4 | 6 | 3 | 6 | 1 | 4 | 3 | 5 | 5 | 1 |265-| . | . | . | . | 3 | 2 | 3 | 1 | 2 | 1 | 2 | 1 | 2 | 4 |266-| . | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |267-| . | . | 2 | 3 | . | . | . | . | . | . | . | . | . | . |268-| . | 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |269-| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |270-| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |271-| . | 4 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |272-| 1 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |273-| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |274-| . | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |275-| . | . | 5 | 2 | . | . | . | . | . | . | . | . | . | . |276-277-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.278-279----280-281-## eager-cloud - 12282-283-### Puzzle Grid with Integrated Hints284-285-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |286-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |287-| . | . | . | . | . | . | . | . | 1 | . | . | . | . | . |288-| . | . | . | . | 1 | 1 | . | . | 2 | 2 | . | . | . | . |289-| . | . | . | . | 6 | 1 | 5 | 3 | 2 | 1 | . | 2 | . | 2 |290-| . | . | . | . | 1 | 2 | 3 | 4 | 1 | 2 | 9 | 4 | 6 | 3 |291-| . | . | 7 | 1 | . | . | . | . | . | . | . | . | . | . |292-| . | 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |293-| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |294-| . | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |295-| 1 | 3 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |296-| . | 1 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |297-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |298-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |299-| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |300-| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |301-302-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.303-304----305-306-## sandy-quartz - 13307-308-### Puzzle Grid with Integrated Hints309-310-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |311-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |312-| . | . | . | . | . | 1 | 1 | . | . | . | . | . | . | . |313-| . | . | . | . | 1 | 1 | 3 | . | . | 3 | 1 | 1 | . | 1 |314-| . | . | . | . | 1 | 1 | 2 | 2 | 2 | 1 | 4 | 6 | 4 | 2 |315-| . | . | . | . | 6 | 1 | 1 | 4 | 3 | 1 | 2 | 1 | 3 | 2 |316-| 1 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |317-| . | . | 2 | 1 | . | . | . | . | . | . | . | . | . | . |318-| . | 3 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |319-| . | . | 3 | 4 | . | . | . | . | . | . | . | . | . | . |320-| . | . | 4 | 2 | . | . | . | . | . | . | . | . | . | . |321-| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |322-| . | 1 | 3 | 4 | . | . | . | . | . | . | . | . | . | . |323-| . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |324-| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |325-| . | . | 4 | 2 | . | . | . | . | . | . | . | . | . | . |326-327-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.328-329----330-331-## cosmic-tide - 14332-333-### Puzzle Grid with Integrated Hints334-335-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |336-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |337-| . | . | . | . | . | . | . | . | . | . | . | . | . | 1 |338-| . | . | . | . | . | . | 1 | 1 | . | . | 1 | . | 1 | 1 |339-| . | . | . | . | 6 | . | 1 | 3 | . | 2 | 3 | 4 | 4 | 2 |340-| . | . | . | . | 3 | 9 | 3 | 4 | 8 | 1 | 2 | 4 | 3 | 1 |341-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |342-| . | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |343-| . | 2 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |344-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |345-| 2 | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |346-| . | 3 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |347-| 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |348-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |349-| . | . | 4 | 3 | . | . | . | . | . | . | . | . | . | . |350-| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |351-352-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.353-354----355-356-## copper-river - 15357-358-### Puzzle Grid with Integrated Hints359-360-| R1 | R2 | R3 | R4 | R5 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |361-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |362-| . | . | . | . | . | . | . | . | 1 | . | . | . | . | . | 3 |363-| . | . | . | . | . | 4 | 2 | 1 | 2 | 2 | . | 1 | 3 | 1 | 1 |364-| . | . | . | . | . | 1 | 3 | 3 | 1 | 1 | 4 | 3 | 1 | 4 | 1 |365-| . | . | . | . | . | 1 | 1 | 4 | 1 | 2 | 3 | 3 | 1 | 2 | 1 |366-| . | . | 5 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |367-| . | . | 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |368-| 1 | 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |369-| . | 1 | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |370-| . | . | . | 2 | 4 | . | . | . | . | . | . | . | . | . | . |371-| . | . | 1 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |372-| . | . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |373-| . | . | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |374-| . | . | . | 3 | 5 | . | . | . | . | . | . | . | . | . | . |375-| . | . | 2 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |376-377-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.378-379----380-381-## deep-badger - 16382-383-### Puzzle Grid with Integrated Hints384-385-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |386-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |387-| . | . | . | . | 1 | . | . | . | . | . | . | . | . |388-| . | . | . | 1 | 1 | . | . | . | . | . | . | . | . |389-| . | . | . | 1 | 2 | 1 | . | . | 4 | . | 2 | . | . |390-| . | . | . | 1 | 1 | 3 | 7 | 3 | 1 | 2 | 5 | 6 | 3 |391-| . | . | . | 2 | 1 | 1 | 1 | 5 | 2 | 4 | 1 | 1 | 2 |392-| 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |393-| . | . | 8 | . | . | . | . | . | . | . | . | . | . |394-| 2 | 4 | 2 | . | . | . | . | . | . | . | . | . | . |395-| . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |396-| . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |397-| . | . | 9 | . | . | . | . | . | . | . | . | . | . |398-| 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |399-| . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |400-| . | 1 | 2 | . | . | . | . | . | . | . | . | . | . |401-| . | 5 | 2 | . | . | . | . | . | . | . | . | . | . |402-403-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.404-405----406-407-## mighty-trout - 17408-409-### Puzzle Grid with Integrated Hints410-411-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |412-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |413-| . | . | . | . | . | . | . | . | . | . | 1 | . | . | 1 |414-| . | . | . | . | 1 | 5 | . | 2 | . | . | 1 | 2 | . | 1 |415-| . | . | . | . | 2 | 1 | 4 | 1 | 2 | 2 | 1 | 5 | 2 | 1 |416-| . | . | . | . | 2 | 1 | 3 | 1 | 4 | 4 | 2 | 1 | 3 | 1 |417-| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |418-| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |419-| . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |420-| 3 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |421-| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |422-| . | . | . | 7 | . | . | . | . | . | . | . | . | . | . |423-| . | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |424-| . | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |425-| . | . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |426-| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |427-428-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.429-430----431-432-## blazing-trout - 18433-434-### Puzzle Grid with Integrated Hints435-436-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |437-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |438-| . | . | . | . | . | . | . | . | . | . | 2 | . | . | . |439-| . | . | . | . | 1 | 1 | 4 | . | . | . | 1 | . | 1 | . |440-| . | . | . | . | 2 | 3 | 1 | 1 | 4 | 3 | 2 | 7 | 2 | 6 |441-| . | . | . | . | 5 | 3 | 2 | 7 | 2 | 6 | 1 | 2 | 4 | 3 |442-| 4 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |443-| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |444-| . | 4 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |445-| . | . | 5 | 3 | . | . | . | . | . | . | . | . | . | . |446-| . | . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |447-| 1 | 4 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |448-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |449-| 2 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |450-| . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |451-| . | 1 | 1 | 6 | . | . | . | . | . | . | . | . | . | . |452-453-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.454-455----456-457-## gilded-ivy - 19458-459-### Puzzle Grid with Integrated Hints460-461-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |462-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |463-| . | . | . | . | . | 3 | . | . | . | . | . | . | . | . |464-| . | . | . | . | . | 2 | 4 | 2 | . | . | . | . | . | 1 |465-| . | . | . | . | 2 | 1 | 1 | 4 | 4 | 6 | 4 | 1 | 6 | 4 |466-| . | . | . | . | 3 | 1 | 1 | 1 | 2 | 2 | 1 | 7 | 3 | 2 |467-| . | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |468-| . | . | 7 | 1 | . | . | . | . | . | . | . | . | . | . |469-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |470-| . | . | 1 | 6 | . | . | . | . | . | . | . | . | . | . |471-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |472-| 1 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |473-| . | . | 2 | 1 | . | . | . | . | . | . | . | . | . | . |474-| . | . | 5 | 2 | . | . | . | . | . | . | . | . | . | . |475-| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |476-| . | 4 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |477-478-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.479-480----481-482-## woven-lagoon - 20483-484-### Puzzle Grid with Integrated Hints485-486-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |487-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |488-| . | . | . | . | 3 | . | . | . | 1 | . | . | 1 | . | . |489-| . | . | . | . | 1 | . | 1 | . | 1 | . | . | 2 | . | 2 |490-| . | . | . | . | 1 | . | 3 | 2 | 1 | 6 | 2 | 1 | 3 | 2 |491-| . | . | . | . | 1 | 3 | 3 | 3 | 2 | 2 | 1 | 1 | 3 | 3 |492-| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |493-| . | 2 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |494-| . | 3 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |495-| . | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |496-| 1 | 4 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |497-| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |498-| . | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |499-| . | 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |500-| . | . | 4 | 2 | . | . | . | . | . | . | . | . | . | . |501-| 1 | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |502-503-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.504-505----506-507-## fading-aurora - 21508-509-### Puzzle Grid with Integrated Hints510-511-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |512-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |513-| . | . | . | 1 | 1 | 1 | 5 | . | 3 | . | 1 | 2 | . |514-| . | . | . | 4 | 4 | 5 | 1 | 5 | 2 | 8 | 2 | 1 | 4 |515-| . | . | . | 1 | 2 | 2 | 1 | 1 | 1 | 1 | 3 | 1 | 1 |516-| 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |517-| . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |518-| 1 | 5 | 2 | . | . | . | . | . | . | . | . | . | . |519-| 5 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |520-| 5 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |521-| . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |522-| . | 2 | 4 | . | . | . | . | . | . | . | . | . | . |523-| 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |524-| 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |525-| . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |526-527-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.528-529----530-531-## crimson-owl - 22532-533-### Puzzle Grid with Integrated Hints534-535-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |536-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |537-| . | . | . | . | . | . | 1 | 1 | 2 | 2 | . | 2 | . | 3 |538-| . | . | . | . | 2 | 2 | 1 | 3 | 4 | 4 | 6 | 5 | 5 | 3 |539-| . | . | . | . | 3 | 1 | 5 | 3 | 1 | 1 | 1 | 1 | 4 | 1 |540-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |541-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |542-| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |543-| . | . | . | 8 | . | . | . | . | . | . | . | . | . | . |544-| . | . | . | 6 | . | . | . | . | . | . | . | . | . | . |545-| . | 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |546-| . | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |547-| . | 1 | 2 | 4 | . | . | . | . | . | . | . | . | . | . |548-| 1 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |549-| . | 1 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |550-551-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.552-553----554-555-## timber-falcon - 23556-557-### Puzzle Grid with Integrated Hints558-559-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |560-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |561-| . | . | . | . | . | . | 2 | . | . | . | . | 2 | . | . |562-| . | . | . | . | . | . | 3 | . | . | . | 2 | 2 | 2 | . |563-| . | . | . | . | . | 5 | 1 | 6 | 6 | . | 1 | 1 | 1 | 1 |564-| . | . | . | . | 9 | 1 | 1 | 2 | 2 | 6 | 4 | 1 | 2 | 5 |565-| . | . | 2 | 2 | . | . | . | . | . | . | . | . | . | . |566-| . | . | 1 | 8 | . | . | . | . | . | . | . | . | . | . |567-| . | 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |568-| . | . | 6 | 1 | . | . | . | . | . | . | . | . | . | . |569-| . | . | 8 | 1 | . | . | . | . | . | . | . | . | . | . |570-| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |571-| . | 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |572-| 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |573-| . | 1 | 2 | 4 | . | . | . | . | . | . | . | . | . | . |574-| . | 5 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |575-576-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.577-578----579-580-## blazing-acorn - 24581-582-### Puzzle Grid with Integrated Hints583-584-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |585-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |586-| . | . | . | . | 1 | . | . | 1 | . | . | . | . | . | . |587-| . | . | . | . | 1 | 1 | 3 | 2 | 3 | . | . | 1 | . | . |588-| . | . | . | . | 1 | 2 | 3 | 1 | 1 | 2 | 5 | 2 | 6 | 2 |589-| . | . | . | . | 2 | 2 | 1 | 2 | 2 | 3 | 2 | 4 | 2 | 4 |590-| . | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |591-| 2 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |592-| . | 1 | 3 | 4 | . | . | . | . | . | . | . | . | . | . |593-| . | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |594-| . | 3 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |595-| . | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |596-| . | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |597-| . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |598-| . | . | 2 | 7 | . | . | . | . | . | . | . | . | . | . |599-| . | . | 6 | 3 | . | . | . | . | . | . | . | . | . | . |600-601-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.602-603----604-605-## onyx-pond - 25606-607-### Puzzle Grid with Integrated Hints608-609-| R1 | R2 | R3 | R4 | R5 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |610-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |611-| . | . | . | . | . | . | . | . | 2 | . | . | . | . | . | . |612-| . | . | . | . | . | 2 | 1 | . | 1 | . | 2 | . | 3 | 2 | . |613-| . | . | . | . | . | 3 | 2 | 5 | 1 | 1 | 1 | 7 | 1 | 1 | . |614-| . | . | . | . | . | 1 | 2 | 4 | 1 | 8 | 3 | 2 | 3 | 2 | 9 |615-| . | . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |616-| . | . | 1 | 2 | 5 | . | . | . | . | . | . | . | . | . | . |617-| 1 | 1 | 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |618-| . | . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |619-| . | 2 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |620-| . | . | 1 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |621-| . | . | 1 | 5 | 1 | . | . | . | . | . | . | . | . | . | . |622-| . | 3 | 2 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |623-| . | . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |624-| . | 1 | 1 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |625-626-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.627-628----629-630-## desert-hollow - 26631-632-### Puzzle Grid with Integrated Hints633-634-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |635-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |636-| . | . | . | . | . | . | . | . | . | . | . | 3 | . | . |637-| . | . | . | . | . | . | 1 | . | . | . | . | 1 | . | 2 |638-| . | . | . | . | 7 | 2 | 5 | 3 | 1 | 4 | 7 | 1 | 7 | 3 |639-| . | . | . | . | 1 | 5 | 1 | 3 | 5 | 4 | 2 | 1 | 2 | 2 |640-| . | . | 1 | 5 | . | . | . | . | . | . | . | . | . | . |641-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |642-| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |643-| . | 1 | 5 | 2 | . | . | . | . | . | . | . | . | . | . |644-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |645-| 3 | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |646-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |647-| . | . | 2 | 3 | . | . | . | . | . | . | . | . | . | . |648-| . | 1 | 1 | 5 | . | . | . | . | . | . | . | . | . | . |649-| 1 | 1 | 2 | 2 | . | . | . | . | . | . | . | . | . | . |650-651-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.652-653----654-655-## emerald-newt - 27656-657-### Puzzle Grid with Integrated Hints658-659-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |660-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |661-| . | . | . | . | . | . | . | 1 | . | . | . | . | . | . |662-| . | . | . | . | 3 | . | . | 1 | 3 | . | . | . | . | . |663-| . | . | . | . | 2 | 6 | 4 | 3 | 1 | 6 | 6 | 7 | 2 | 2 |664-| . | . | . | . | 1 | 3 | 3 | 1 | 2 | 2 | 2 | 1 | 3 | 6 |665-| . | . | 1 | 4 | . | . | . | . | . | . | . | . | . | . |666-| . | 2 | 4 | 1 | . | . | . | . | . | . | . | . | . | . |667-| . | . | 8 | 1 | . | . | . | . | . | . | . | . | . | . |668-| . | . | 3 | 4 | . | . | . | . | . | . | . | . | . | . |669-| . | . | 4 | 5 | . | . | . | . | . | . | . | . | . | . |670-| . | 1 | 5 | 1 | . | . | . | . | . | . | . | . | . | . |671-| . | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |672-| . | 3 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |673-| . | 2 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |674-| 2 | 1 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |675-676-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.677-678----679-680-## velvet-viper - 28681-682-### Puzzle Grid with Integrated Hints683-684-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |685-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |686-| . | . | . | . | . | . | . | 1 | 2 | 1 | . | . | . | . |687-| . | . | . | . | 1 | 2 | . | 4 | 1 | 3 | 1 | 2 | 4 | 1 |688-| . | . | . | . | 3 | 1 | 2 | 1 | 1 | 1 | 4 | 4 | 2 | 5 |689-| . | . | . | . | 3 | 2 | 6 | 1 | 3 | 1 | 1 | 1 | 1 | 1 |690-| . | . | 1 | 8 | . | . | . | . | . | . | . | . | . | . |691-| . | 1 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |692-| 1 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |693-| . | 2 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |694-| . | 4 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |695-| . | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |696-| . | . | 3 | 4 | . | . | . | . | . | . | . | . | . | . |697-| . | 1 | 5 | 2 | . | . | . | . | . | . | . | . | . | . |698-| . | . | 3 | 1 | . | . | . | . | . | . | . | . | . | . |699-| . | . | . | 9 | . | . | . | . | . | . | . | . | . | . |700-701-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.702-703----704-705-## crystal-larch - 29706-707-### Puzzle Grid with Integrated Hints708-709-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |710-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |711-| . | . | . | . | . | . | . | 3 | . | . | 1 | . | 1 | 1 |712-| . | . | . | . | . | . | . | 1 | 2 | . | 1 | 1 | 1 | 1 |713-| . | . | . | . | 1 | . | . | 1 | 3 | . | 2 | 2 | 3 | 3 |714-| . | . | . | . | 6 | 7 | 6 | 1 | 3 | 8 | 1 | 2 | 1 | 1 |715-| . | 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |716-| . | . | 5 | 1 | . | . | . | . | . | . | . | . | . | . |717-| 3 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |718-| . | . | 2 | 5 | . | . | . | . | . | . | . | . | . | . |719-| . | . | 3 | 2 | . | . | . | . | . | . | . | . | . | . |720-| . | . | 7 | 2 | . | . | . | . | . | . | . | . | . | . |721-| . | . | 3 | 5 | . | . | . | . | . | . | . | . | . | . |722-| . | 1 | 3 | 3 | . | . | . | . | . | . | . | . | . | . |723-| . | . | 1 | 3 | . | . | . | . | . | . | . | . | . | . |724-| . | 1 | 3 | 2 | . | . | . | . | . | . | . | . | . | . |725-726-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.727-728----729-730-## aqua-robin - 30731-732-### Puzzle Grid with Integrated Hints733-734-| R1 | R2 | R3 | R4 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |735-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |736-| . | . | . | . | . | . | . | . | . | . | 1 | . | . | . |737-| . | . | . | . | . | . | . | . | 5 | . | 1 | . | . | . |738-| . | . | . | . | 5 | 2 | 3 | 3 | 1 | 6 | 2 | 5 | 6 | 2 |739-| . | . | . | . | 4 | 7 | 5 | 2 | 1 | 1 | 3 | 3 | 3 | 3 |740-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |741-| . | . | 6 | 2 | . | . | . | . | . | . | . | . | . | . |742-| . | . | 1 | 7 | . | . | . | . | . | . | . | . | . | . |743-| . | 3 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |744-| . | . | 2 | 6 | . | . | . | . | . | . | . | . | . | . |745-| . | 2 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |746-| 3 | 1 | 1 | 1 | . | . | . | . | . | . | . | . | . | . |747-| . | . | 4 | 4 | . | . | . | . | . | . | . | . | . | . |748-| . | . | 5 | 4 | . | . | . | . | . | . | . | . | . | . |749-| . | 3 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |750-751-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.752-753----754-755-## starry-trout - 31756-757-### Puzzle Grid with Integrated Hints758-759-| R1 | R2 | R3 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 |760-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |761-| . | . | . | . | . | 1 | . | . | . | . | . | 2 | . |762-| . | . | . | . | 2 | 1 | 4 | 2 | 1 | 1 | . | 1 | . |763-| . | . | . | 2 | 2 | 2 | 2 | 2 | 1 | 1 | 4 | 2 | 7 |764-| . | . | . | 4 | 4 | 2 | 2 | 3 | 1 | 5 | 3 | 1 | 2 |765-| . | 3 | 5 | . | . | . | . | . | . | . | . | . | . |766-| 1 | 2 | 3 | . | . | . | . | . | . | . | . | . | . |767-| 1 | 6 | 1 | . | . | . | . | . | . | . | . | . | . |768-| 2 | 1 | 3 | . | . | . | . | . | . | . | . | . | . |769-| 2 | 3 | 1 | . | . | . | . | . | . | . | . | . | . |770-| 1 | 3 | 4 | . | . | . | . | . | . | . | . | . | . |771-| 2 | 1 | 4 | . | . | . | . | . | . | . | . | . | . |772-| 3 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |773-| 5 | 1 | 2 | . | . | . | . | . | . | . | . | . | . |774-| 1 | 2 | 1 | . | . | . | . | . | . | . | . | . | . |775-776-Row hints are right-aligned beside each row. Column hints are stacked above each column and bottom-aligned to the grid.
A pdfexport/cover_art.go +841 -0
@@ -0,0 +1,841 @@
1+package pdfexport2+3+import (4+ "bytes"5+ "encoding/binary"6+ "fmt"7+ "hash/fnv"8+ "image"9+ "image/color"10+ "image/png"11+ "math"12+ "math/rand"13+ "strings"14+ "time"15+16+ "github.com/go-pdf/fpdf"17+)18+19+type coverVec2 struct {20+ x float6421+ y float6422+}23+24+type coverGlow struct {25+ center coverVec226+ spread float6427+ strength float6428+ col color.RGBA29+}30+31+type coverFieldProfile struct {32+ freqX float6433+ freqY float6434+ freqMixX float6435+ freqMixY float6436+ freqCurlX float6437+ freqCurlY float6438+ ampX float6439+ ampY float6440+ ampMix float6441+ ampCurl float6442+ phaseX float6443+ phaseY float6444+ phaseMix float6445+ phaseCurl float6446+ swirl float6447+ shear float6448+ pinch float6449+ pivot coverVec250+}51+52+type coverFlowLayer struct {53+ count int54+ steps int55+ step float6456+ alpha uint857+ radius float6458+ drift float6459+ phase float6460+ waveXFreq float6461+ waveYFreq float6462+ waveXAmp float6463+ waveYAmp float6464+ a color.RGBA65+ b color.RGBA66+}67+68+type coverLatticeProfile struct {69+ enabled bool70+ layout int71+ cols int72+ rows int73+ neighbors int74+ jitterX float6475+ jitterY float6476+ center coverVec277+ radialWarp float6478+ edge color.RGBA79+ nodeOuter color.RGBA80+ nodeInner color.RGBA81+ nodeSize float6482+ coreSize float6483+}84+85+type coverOrbitProfile struct {86+ enabled bool87+ center coverVec288+ count int89+ radiusStart float6490+ radiusStep float6491+ arcCoverage float6492+ segmentsMin int93+ segmentsJitter int94+ eccentricity float6495+ wobble float6496+ dotSize float6497+ colorA color.RGBA98+ colorB color.RGBA99+ alphaBase uint8100+ alphaStep uint8101+}102+103+type coverMarkProfile struct {104+ enabled bool105+ count int106+ gridW int107+ gridH int108+ jitter float64109+ sizeMin float64110+ sizeMax float64111+ alphaBase uint8112+ alphaRange uint8113+ colorA color.RGBA114+ colorB color.RGBA115+ cutout color.RGBA116+}117+118+type coverArtDirection struct {119+ motif int120+ top color.RGBA121+ mid color.RGBA122+ bottom color.RGBA123+ verticalCurve float64124+ glows []coverGlow125+ field coverFieldProfile126+ flowLayers []coverFlowLayer127+ lattice coverLatticeProfile128+ orbit coverOrbitProfile129+ marks coverMarkProfile130+ grainCount int131+ grainWarm color.RGBA132+ grainCool color.RGBA133+}134+135+func drawCoverArtworkImage(136+ pdf *fpdf.Fpdf,137+ scene rectMM,138+ seedText string,139+ variant string,140+ base RGB,141+) {142+ seedText = strings.TrimSpace(seedText)143+ if seedText == "" {144+ seedText = "puzzletea" + time.Now().String()145+ }146+ if strings.TrimSpace(variant) == "" {147+ variant = "front"148+ }149+ imageName := fmt.Sprintf(150+ "puzzletea-cover-artwork-%016x",151+ coverSeedHash(seedText+"|"+variant),152+ )153+ artPNG := renderCoverArtworkPNG(seedText, variant, base)154+ options := fpdf.ImageOptions{155+ ImageType: "PNG",156+ ReadDpi: true,157+ }158+ pdf.RegisterImageOptionsReader(imageName, options, bytes.NewReader(artPNG))159+ pdf.ImageOptions(imageName, scene.x, scene.y, scene.w, scene.h, false, options, 0, "")160+}161+162+func renderCoverArtworkPNG(seedText, variant string, base RGB) []byte {163+ const (164+ width = 1200165+ height = 1400166+ )167+ seed := coverSeedHash(seedText + "|" + variant)168+ direction := newCoverArtDirection(seed, base)169+ img := image.NewRGBA(image.Rect(0, 0, width, height))170+171+ paintCoverBackground(img, width, height, direction)172+ drawCoverFlowTrails(img, width, height, direction, coverStreamRNG(seed, "flow"))173+ drawCoverPuzzleLattice(img, width, height, direction, coverStreamRNG(seed, "lattice"))174+ drawCoverOrbitBands(img, width, height, direction, coverStreamRNG(seed, "orbit"))175+ drawCoverSeedMarks(img, width, height, direction, coverStreamRNG(seed, "marks"))176+ drawCoverFilmGrain(img, width, height, direction, coverStreamRNG(seed, "grain"))177+178+ var out bytes.Buffer179+ if err := png.Encode(&out, img); err != nil {180+ return []byte{}181+ }182+ return out.Bytes()183+}184+185+func newCoverArtDirection(seed uint64, base RGB) coverArtDirection {186+ rng := coverStreamRNG(seed, "direction")187+ motif := int(seed % 5)188+189+ warm := blendRGB(190+ brightenRGB(base, 0.30+rng.Float64()*0.28),191+ color.RGBA{R: 255, G: 230, B: 180, A: 255},192+ 0.34+rng.Float64()*0.38,193+ )194+ cool := blendRGB(195+ brightenRGB(base, 0.18+rng.Float64()*0.24),196+ color.RGBA{R: 88, G: 212, B: 230, A: 255},197+ 0.30+rng.Float64()*0.42,198+ )199+ dusk := blendRGB(200+ darkenRGB(base, 0.50+rng.Float64()*0.22),201+ color.RGBA{R: 9, G: 18, B: 32, A: 255},202+ 0.44+rng.Float64()*0.30,203+ )204+ ink := blendRGB(dusk, color.RGBA{R: 22, G: 36, B: 52, A: 255}, 0.40+rng.Float64()*0.25)205+206+ top := blendRGB(warm, color.RGBA{R: 255, G: 255, B: 251, A: 255}, 0.15+rng.Float64()*0.18)207+ mid := blendRGB(cool, warm, 0.16+rng.Float64()*0.26)208+ bottom := blendRGB(dusk, color.RGBA{R: 4, G: 9, B: 16, A: 255}, 0.30+rng.Float64()*0.22)209+210+ glows := make([]coverGlow, 0, 4)211+ glowCount := 3 + rng.Intn(2)212+ for i := 0; i < glowCount; i++ {213+ col := warm214+ if i%2 == 1 {215+ col = cool216+ }217+ col = jitterRGBA(col, rng, 18)218+ glows = append(glows, coverGlow{219+ center: coverVec2{220+ x: 0.08 + rng.Float64()*0.84,221+ y: 0.10 + rng.Float64()*0.82,222+ },223+ spread: 0.42 + rng.Float64()*0.70,224+ strength: 0.16 + rng.Float64()*0.50,225+ col: col,226+ })227+ }228+229+ field := coverFieldProfile{230+ freqX: 4.0 + rng.Float64()*5.8,231+ freqY: 3.8 + rng.Float64()*5.6,232+ freqMixX: 0.5 + rng.Float64()*1.7,233+ freqMixY: 0.5 + rng.Float64()*1.7,234+ freqCurlX: 0.4 + rng.Float64()*1.9,235+ freqCurlY: 0.4 + rng.Float64()*1.9,236+ ampX: 0.65 + rng.Float64()*0.95,237+ ampY: 0.65 + rng.Float64()*0.95,238+ ampMix: 0.28 + rng.Float64()*0.86,239+ ampCurl: 0.20 + rng.Float64()*0.86,240+ phaseX: 0.25 + rng.Float64()*1.40,241+ phaseY: 0.25 + rng.Float64()*1.40,242+ phaseMix: 0.20 + rng.Float64()*1.10,243+ phaseCurl: 0.20 + rng.Float64()*1.10,244+ swirl: (rng.Float64()*2 - 1) * 0.34,245+ shear: (rng.Float64()*2 - 1) * 0.34,246+ pinch: (rng.Float64()*2 - 1) * 0.52,247+ pivot: coverVec2{248+ x: 0.22 + rng.Float64()*0.56,249+ y: 0.20 + rng.Float64()*0.62,250+ },251+ }252+253+ flowLayers := make([]coverFlowLayer, 0, 4)254+ layerCount := 2 + rng.Intn(3)255+ for i := 0; i < layerCount; i++ {256+ t := float64(i) / float64(maxInt(1, layerCount-1))257+ a := jitterRGBA(lerpRGB(warm, cool, 0.22+t*0.54), rng, 20)258+ b := jitterRGBA(lerpRGB(cool, warm, 0.18+t*0.66), rng, 20)259+ flowLayers = append(flowLayers, coverFlowLayer{260+ count: 250 + rng.Intn(320),261+ steps: 90 + rng.Intn(120),262+ step: 1.18 + rng.Float64()*1.72,263+ alpha: uint8(14 + rng.Intn(38)),264+ radius: 0.76 + rng.Float64()*0.86,265+ drift: 0.44 + rng.Float64()*1.68,266+ phase: rng.Float64() * math.Pi * 2,267+ waveXFreq: 5.0 + rng.Float64()*7.2,268+ waveYFreq: 5.0 + rng.Float64()*7.2,269+ waveXAmp: 0.05 + rng.Float64()*0.33,270+ waveYAmp: 0.05 + rng.Float64()*0.33,271+ a: a,272+ b: b,273+ })274+ }275+276+ lattice := coverLatticeProfile{277+ enabled: true,278+ layout: rng.Intn(3),279+ cols: 7 + rng.Intn(6),280+ rows: 6 + rng.Intn(6),281+ neighbors: 2 + rng.Intn(3),282+ jitterX: 24 + rng.Float64()*46,283+ jitterY: 28 + rng.Float64()*52,284+ center: coverVec2{x: 0.30 + rng.Float64()*0.40, y: 0.26 + rng.Float64()*0.44},285+ radialWarp: (rng.Float64()*2 - 1) * 0.34,286+ edge: color.RGBA{R: ink.R, G: ink.G, B: ink.B, A: uint8(40 + rng.Intn(60))},287+ nodeOuter: blendRGB(warm, color.RGBA{R: 255, G: 245, B: 218, A: 255}, 0.35),288+ nodeInner: blendRGB(dusk, color.RGBA{R: 16, G: 30, B: 48, A: 255}, 0.25),289+ nodeSize: 3.6 + rng.Float64()*3.6,290+ coreSize: 1.5 + rng.Float64()*1.6,291+ }292+293+ orbit := coverOrbitProfile{294+ enabled: true,295+ center: coverVec2{x: 0.34 + rng.Float64()*0.32, y: 0.30 + rng.Float64()*0.32},296+ count: 8 + rng.Intn(18),297+ radiusStart: 40 + rng.Float64()*50,298+ radiusStep: 15 + rng.Float64()*18,299+ arcCoverage: 0.42 + rng.Float64()*0.58,300+ segmentsMin: 38 + rng.Intn(34),301+ segmentsJitter: 46 + rng.Intn(52),302+ eccentricity: 0.70 + rng.Float64()*0.48,303+ wobble: 2.8 + rng.Float64()*8.4,304+ dotSize: 0.86 + rng.Float64()*0.76,305+ colorA: jitterRGBA(lerpRGB(warm, cool, 0.32), rng, 16),306+ colorB: jitterRGBA(lerpRGB(cool, warm, 0.70), rng, 16),307+ alphaBase: uint8(12 + rng.Intn(20)),308+ alphaStep: uint8(1 + rng.Intn(2)),309+ }310+311+ marks := coverMarkProfile{312+ enabled: true,313+ count: 24 + rng.Intn(54),314+ gridW: 8 + rng.Intn(8),315+ gridH: 10 + rng.Intn(9),316+ jitter: 8 + rng.Float64()*28,317+ sizeMin: 1.8 + rng.Float64()*1.3,318+ sizeMax: 3.8 + rng.Float64()*2.8,319+ alphaBase: uint8(32 + rng.Intn(36)),320+ alphaRange: uint8(12 + rng.Intn(32)),321+ colorA: jitterRGBA(lerpRGB(warm, cool, 0.20), rng, 20),322+ colorB: jitterRGBA(lerpRGB(cool, warm, 0.72), rng, 20),323+ cutout: blendRGB(dusk, color.RGBA{R: 5, G: 10, B: 18, A: 255}, 0.35),324+ }325+326+ direction := coverArtDirection{327+ motif: motif,328+ top: top,329+ mid: mid,330+ bottom: bottom,331+ verticalCurve: 1.24 + rng.Float64()*1.08,332+ glows: glows,333+ field: field,334+ flowLayers: flowLayers,335+ lattice: lattice,336+ orbit: orbit,337+ marks: marks,338+ grainCount: 32000 + rng.Intn(34000),339+ grainWarm: blendRGB(warm, color.RGBA{R: 255, G: 246, B: 226, A: 255}, 0.42),340+ grainCool: blendRGB(dusk, color.RGBA{R: 5, G: 12, B: 20, A: 255}, 0.52),341+ }342+343+ switch direction.motif {344+ case 0:345+ direction.lattice.layout = 0346+ direction.lattice.neighbors = minInt(5, direction.lattice.neighbors+1)347+ direction.orbit.arcCoverage = clamp01(direction.orbit.arcCoverage*0.72 + 0.10)348+ direction.marks.count += 10349+ case 1:350+ for i := range direction.flowLayers {351+ direction.flowLayers[i].count += 120352+ direction.flowLayers[i].steps += 18353+ direction.flowLayers[i].alpha = minUint8(70, direction.flowLayers[i].alpha+12)354+ direction.flowLayers[i].drift *= 1.24355+ }356+ direction.lattice.edge.A = maxUint8(18, direction.lattice.edge.A-22)357+ direction.orbit.count = maxInt(6, direction.orbit.count/2)358+ direction.grainCount += 6000359+ case 2:360+ direction.orbit.count += 12361+ direction.orbit.arcCoverage = clamp01(direction.orbit.arcCoverage*1.18 + 0.08)362+ direction.orbit.radiusStep *= 0.86363+ direction.lattice.enabled = rng.Float64() > 0.26364+ for i := range direction.flowLayers {365+ direction.flowLayers[i].count = maxInt(180, direction.flowLayers[i].count-60)366+ }367+ case 3:368+ direction.lattice.layout = 2369+ direction.marks.count += 30370+ direction.marks.sizeMax += 1.4371+ direction.orbit.arcCoverage *= 0.74372+ direction.field.swirl *= 1.25373+ case 4:374+ direction.lattice.layout = 1375+ direction.lattice.rows += 2376+ direction.orbit.eccentricity = clamp(0.55, 1.42, direction.orbit.eccentricity*1.16)377+ direction.field.pinch *= 1.34378+ direction.grainCount += 9000379+ }380+381+ return direction382+}383+384+func paintCoverBackground(img *image.RGBA, w, h int, direction coverArtDirection) {385+ for y := range h {386+ ty := float64(y) / float64(h-1)387+ vertical := blendRGB(388+ lerpRGB(direction.top, direction.mid, ty*1.12),389+ direction.bottom,390+ powClamp(ty, direction.verticalCurve),391+ )392+ for x := range w {393+ tx := float64(x) / float64(w-1)394+ col := vertical395+ for _, glow := range direction.glows {396+ amount := radialFalloff(tx, ty, glow.center.x, glow.center.y, glow.spread)397+ col = blendRGB(col, glow.col, amount*glow.strength)398+ }399+ img.SetRGBA(x, y, col)400+ }401+ }402+}403+404+func drawCoverFlowTrails(405+ img *image.RGBA,406+ w, h int,407+ direction coverArtDirection,408+ rng *rand.Rand,409+) {410+ for _, layer := range direction.flowLayers {411+ for i := 0; i < layer.count; i++ {412+ x := rng.Float64() * float64(w)413+ y := rng.Float64() * float64(h)414+ for step := 0; step < layer.steps; step++ {415+ nx := x / float64(w)416+ ny := y / float64(h)417+ angle := coverFieldAngle(nx, ny, direction.field, layer.phase)418+ angle += math.Sin((ny+layer.phase)*layer.waveYFreq) * layer.waveYAmp419+ angle += math.Cos((nx-layer.phase)*layer.waveXFreq) * layer.waveXAmp420+421+ x += math.Cos(angle) * layer.step422+ y += math.Sin(angle) * layer.step423+ x += math.Cos((ny-layer.phase)*math.Pi*2) * layer.drift * 0.04424+ y += math.Sin((nx+layer.phase)*math.Pi*2) * layer.drift * 0.08425+ if x < 1 || x >= float64(w-1) || y < 1 || y >= float64(h-1) {426+ break427+ }428+ t := float64(step) / float64(layer.steps)429+ c := lerpRGB(layer.a, layer.b, t)430+ c.A = layer.alpha431+ drawDisc(img, x, y, layer.radius, c)432+ }433+ }434+ }435+}436+437+func drawCoverPuzzleLattice(438+ img *image.RGBA,439+ w, h int,440+ direction coverArtDirection,441+ rng *rand.Rand,442+) {443+ if !direction.lattice.enabled {444+ return445+ }446+ nodes := buildCoverLatticeNodes(w, h, direction.lattice, rng)447+ if len(nodes) == 0 {448+ return449+ }450+451+ for i := range nodes {452+ for _, j := range nearestN(nodes, i, direction.lattice.neighbors) {453+ if j > i {454+ drawLine(img, nodes[i], nodes[j], direction.lattice.edge)455+ }456+ }457+ }458+ for _, n := range nodes {459+ drawDisc(img, n.x, n.y, direction.lattice.nodeSize, direction.lattice.nodeOuter)460+ drawDisc(img, n.x, n.y, direction.lattice.coreSize, direction.lattice.nodeInner)461+ }462+}463+464+func buildCoverLatticeNodes(465+ w, h int,466+ profile coverLatticeProfile,467+ rng *rand.Rand,468+) []coverVec2 {469+ nodes := make([]coverVec2, 0, profile.cols*profile.rows)470+ switch profile.layout {471+ case 1:472+ minDim := math.Min(float64(w), float64(h))473+ rings := maxInt(4, profile.rows+1)474+ spokes := maxInt(8, profile.cols+4)475+ center := coverVec2{476+ x: profile.center.x * float64(w),477+ y: profile.center.y * float64(h),478+ }479+ for ring := 1; ring <= rings; ring++ {480+ ringT := float64(ring) / float64(rings+1)481+ radius := ringT * minDim * (0.16 + 0.62*ringT)482+ offset := rng.Float64() * math.Pi * 2483+ for spoke := 0; spoke < spokes; spoke++ {484+ ang := offset + float64(spoke)/float64(spokes)*math.Pi*2485+ ang += math.Sin(float64(ring)*0.9+float64(spoke)*0.45) * profile.radialWarp486+ jitter := (rng.Float64() - 0.5) * profile.jitterX487+ x := center.x + math.Cos(ang)*(radius+jitter)488+ y := center.y + math.Sin(ang)*(radius+jitter*0.45)489+ nodes = append(nodes, coverVec2{x: x, y: y})490+ }491+ }492+ case 2:493+ total := maxInt(38, profile.cols*profile.rows)494+ for i := 0; i < total; i++ {495+ tx := (rng.Float64()*0.84 + 0.08)496+ ty := (rng.Float64()*0.84 + 0.08)497+ warpX := math.Sin(ty*math.Pi*4+float64(i)*0.22) * profile.radialWarp * 0.12498+ warpY := math.Cos(tx*math.Pi*3+float64(i)*0.18) * profile.radialWarp * 0.10499+ x := (tx+warpX)*float64(w) + (rng.Float64()-0.5)*profile.jitterX500+ y := (ty+warpY)*float64(h) + (rng.Float64()-0.5)*profile.jitterY501+ nodes = append(nodes, coverVec2{x: x, y: y})502+ }503+ default:504+ for row := 0; row < profile.rows; row++ {505+ for col := 0; col < profile.cols; col++ {506+ x := (float64(col) + 1) / (float64(profile.cols) + 1) * float64(w)507+ y := (float64(row) + 1) / (float64(profile.rows) + 1) * float64(h)508+ x += (rng.Float64() - 0.5) * profile.jitterX509+ y += (rng.Float64() - 0.5) * profile.jitterY510+ nodes = append(nodes, coverVec2{x: x, y: y})511+ }512+ }513+ }514+ return nodes515+}516+517+func drawCoverOrbitBands(518+ img *image.RGBA,519+ w, h int,520+ direction coverArtDirection,521+ rng *rand.Rand,522+) {523+ if !direction.orbit.enabled {524+ return525+ }526+ center := coverVec2{527+ x: float64(w) * direction.orbit.center.x,528+ y: float64(h) * direction.orbit.center.y,529+ }530+ for i := 0; i < direction.orbit.count; i++ {531+ radius := direction.orbit.radiusStart + float64(i)*direction.orbit.radiusStep532+ start := rng.Float64() * math.Pi * 2533+ sweep := direction.orbit.arcCoverage * (0.74 + rng.Float64()*0.52) * math.Pi * 2534+ sweep = math.Min(sweep, math.Pi*2)535+ segments := direction.orbit.segmentsMin + rng.Intn(direction.orbit.segmentsJitter)536+ t := float64(i) / float64(maxInt(1, direction.orbit.count-1))537+ col := lerpRGB(direction.orbit.colorA, direction.orbit.colorB, t)538+ col.A = minUint8(220, direction.orbit.alphaBase+uint8(i)*direction.orbit.alphaStep)539+ for s := 0; s < segments; s++ {540+ a := start + float64(s)/float64(segments)*sweep541+ jx := math.Sin(float64(i)*0.43+a*2.9) * direction.orbit.wobble542+ jy := math.Cos(float64(i)*0.37+a*2.3) * direction.orbit.wobble * 0.82543+ x := center.x + math.Cos(a)*radius*direction.orbit.eccentricity + jx544+ y := center.y + math.Sin(a)*radius + jy545+ drawDisc(img, x, y, direction.orbit.dotSize, col)546+ }547+ }548+}549+550+func drawCoverSeedMarks(551+ img *image.RGBA,552+ w, h int,553+ direction coverArtDirection,554+ rng *rand.Rand,555+) {556+ if !direction.marks.enabled {557+ return558+ }559+ for i := 0; i < direction.marks.count; i++ {560+ gx := 1 + rng.Intn(direction.marks.gridW)561+ gy := 1 + rng.Intn(direction.marks.gridH)562+ x := float64(gx) / float64(direction.marks.gridW+1) * float64(w)563+ y := float64(gy) / float64(direction.marks.gridH+1) * float64(h)564+ x += (rng.Float64() - 0.5) * direction.marks.jitter565+ y += (rng.Float64() - 0.5) * direction.marks.jitter566+567+ size := direction.marks.sizeMin568+ if direction.marks.sizeMax > direction.marks.sizeMin {569+ size += rng.Float64() * (direction.marks.sizeMax - direction.marks.sizeMin)570+ }571+ col := lerpRGB(direction.marks.colorA, direction.marks.colorB, rng.Float64())572+ col.A = minUint8(220, direction.marks.alphaBase+uint8(rng.Intn(int(direction.marks.alphaRange)+1)))573+ style := (i + rng.Intn(4) + direction.motif) % 4574+ switch style {575+ case 0:576+ drawDisc(img, x, y, size*0.48, col)577+ case 1:578+ drawDisc(img, x, y, size, col)579+ drawDisc(img, x, y, size*0.52, direction.marks.cutout)580+ case 2:581+ drawLine(img, coverVec2{x: x - size, y: y}, coverVec2{x: x + size, y: y}, col)582+ drawLine(img, coverVec2{x: x, y: y - size}, coverVec2{x: x, y: y + size}, col)583+ default:584+ drawLine(img, coverVec2{x: x - size, y: y - size}, coverVec2{x: x + size, y: y + size}, col)585+ drawLine(img, coverVec2{x: x - size, y: y + size}, coverVec2{x: x + size, y: y - size}, col)586+ drawDisc(img, x, y, size*0.32, direction.marks.cutout)587+ }588+ }589+}590+591+func drawCoverFilmGrain(592+ img *image.RGBA,593+ w, h int,594+ direction coverArtDirection,595+ rng *rand.Rand,596+) {597+ for i := 0; i < direction.grainCount; i++ {598+ x := rng.Intn(w)599+ y := rng.Intn(h)600+ alpha := uint8(8 + rng.Intn(18))601+ if rng.Intn(2) == 0 {602+ blendPixel(img, x, y, color.RGBA{603+ R: direction.grainWarm.R,604+ G: direction.grainWarm.G,605+ B: direction.grainWarm.B,606+ A: alpha,607+ })608+ continue609+ }610+ blendPixel(img, x, y, color.RGBA{611+ R: direction.grainCool.R,612+ G: direction.grainCool.G,613+ B: direction.grainCool.B,614+ A: alpha,615+ })616+ }617+}618+619+func coverFieldAngle(x, y float64, field coverFieldProfile, phase float64) float64 {620+ ax := math.Sin((x*field.freqX+phase*field.phaseX)*math.Pi*2) * field.ampX621+ ay := math.Cos((y*field.freqY-phase*field.phaseY)*math.Pi*2) * field.ampY622+ mix := math.Sin((x*field.freqMixX+y*field.freqMixY+phase*field.phaseMix)*math.Pi*2) * field.ampMix623+ curl := math.Cos((x*field.freqCurlX-y*field.freqCurlY+phase*field.phaseCurl)*math.Pi*2) * field.ampCurl624+ radial := math.Atan2(y-field.pivot.y, x-field.pivot.x)625+ distance := math.Hypot(x-field.pivot.x, y-field.pivot.y)626+ swirl := radial * field.swirl627+ pinch := (0.5 - distance) * field.pinch628+ shear := (x - y) * field.shear629+ return ax + ay + mix + curl + swirl + pinch + shear630+}631+632+func nearestN(nodes []coverVec2, idx, n int) []int {633+ n = maxInt(1, n)634+ bestDistance := make([]float64, n)635+ bestIndex := make([]int, n)636+ for i := range bestDistance {637+ bestDistance[i] = math.MaxFloat64638+ bestIndex[i] = -1639+ }640+ for j := range nodes {641+ if j == idx {642+ continue643+ }644+ dx := nodes[idx].x - nodes[j].x645+ dy := nodes[idx].y - nodes[j].y646+ distance := dx*dx + dy*dy647+ for k := range bestDistance {648+ if distance >= bestDistance[k] {649+ continue650+ }651+ for shift := len(bestDistance) - 1; shift > k; shift-- {652+ bestDistance[shift] = bestDistance[shift-1]653+ bestIndex[shift] = bestIndex[shift-1]654+ }655+ bestDistance[k] = distance656+ bestIndex[k] = j657+ break658+ }659+ }660+ out := make([]int, 0, len(bestIndex))661+ for _, best := range bestIndex {662+ if best >= 0 {663+ out = append(out, best)664+ }665+ }666+ return out667+}668+669+func drawDisc(img *image.RGBA, cx, cy, radius float64, c color.RGBA) {670+ minX := int(math.Floor(cx - radius))671+ maxX := int(math.Ceil(cx + radius))672+ minY := int(math.Floor(cy - radius))673+ maxY := int(math.Ceil(cy + radius))674+ r2 := radius * radius675+ for y := minY; y <= maxY; y++ {676+ for x := minX; x <= maxX; x++ {677+ dx := float64(x) + 0.5 - cx678+ dy := float64(y) + 0.5 - cy679+ if dx*dx+dy*dy <= r2 {680+ blendPixel(img, x, y, c)681+ }682+ }683+ }684+}685+686+func drawLine(img *image.RGBA, a, b coverVec2, c color.RGBA) {687+ dx := b.x - a.x688+ dy := b.y - a.y689+ steps := int(math.Max(math.Abs(dx), math.Abs(dy)))690+ if steps <= 0 {691+ blendPixel(img, int(a.x), int(a.y), c)692+ return693+ }694+ for i := 0; i <= steps; i++ {695+ t := float64(i) / float64(steps)696+ drawDisc(img, a.x+dx*t, a.y+dy*t, 0.82, c)697+ }698+}699+700+func blendPixel(img *image.RGBA, x, y int, src color.RGBA) {701+ if !image.Pt(x, y).In(img.Rect) {702+ return703+ }704+ dst := img.RGBAAt(x, y)705+ alpha := float64(src.A) / 255706+ inv := 1 - alpha707+ img.SetRGBA(x, y, color.RGBA{708+ R: uint8(float64(src.R)*alpha + float64(dst.R)*inv),709+ G: uint8(float64(src.G)*alpha + float64(dst.G)*inv),710+ B: uint8(float64(src.B)*alpha + float64(dst.B)*inv),711+ A: 255,712+ })713+}714+715+func brightenRGB(base RGB, amount float64) color.RGBA {716+ return blendRGB(rgbToColor(base), color.RGBA{R: 255, G: 255, B: 255, A: 255}, amount)717+}718+719+func darkenRGB(base RGB, amount float64) color.RGBA {720+ return blendRGB(rgbToColor(base), color.RGBA{R: 0, G: 0, B: 0, A: 255}, amount)721+}722+723+func rgbToColor(c RGB) color.RGBA {724+ return color.RGBA{R: c.R, G: c.G, B: c.B, A: 255}725+}726+727+func lerpRGB(a, b color.RGBA, t float64) color.RGBA {728+ t = clamp01(t)729+ return color.RGBA{730+ R: uint8(float64(a.R) + (float64(b.R)-float64(a.R))*t),731+ G: uint8(float64(a.G) + (float64(b.G)-float64(a.G))*t),732+ B: uint8(float64(a.B) + (float64(b.B)-float64(a.B))*t),733+ A: 255,734+ }735+}736+737+func blendRGB(base, top color.RGBA, amount float64) color.RGBA {738+ return lerpRGB(base, top, clamp01(amount))739+}740+741+func radialFalloff(x, y, cx, cy, spread float64) float64 {742+ dx := x - cx743+ dy := y - cy744+ distance := math.Sqrt(dx*dx + dy*dy)745+ if distance >= spread {746+ return 0747+ }748+ q := 1 - distance/spread749+ return q * q750+}751+752+func powClamp(v, p float64) float64 {753+ return math.Pow(clamp01(v), p)754+}755+756+func clamp01(v float64) float64 {757+ if v < 0 {758+ return 0759+ }760+ if v > 1 {761+ return 1762+ }763+ return v764+}765+766+func clamp(min, max, v float64) float64 {767+ if v < min {768+ return min769+ }770+ if v > max {771+ return max772+ }773+ return v774+}775+776+func maxInt(a, b int) int {777+ if a > b {778+ return a779+ }780+ return b781+}782+783+func minInt(a, b int) int {784+ if a < b {785+ return a786+ }787+ return b788+}789+790+func maxUint8(a, b uint8) uint8 {791+ if a > b {792+ return a793+ }794+ return b795+}796+797+func minUint8(a, b uint8) uint8 {798+ if a < b {799+ return a800+ }801+ return b802+}803+804+func jitterRGBA(c color.RGBA, rng *rand.Rand, spread int) color.RGBA {805+ if spread <= 0 {806+ return c807+ }808+ half := spread / 2809+ return color.RGBA{810+ R: shiftChannel(c.R, rng.Intn(spread)-half),811+ G: shiftChannel(c.G, rng.Intn(spread)-half),812+ B: shiftChannel(c.B, rng.Intn(spread)-half),813+ A: c.A,814+ }815+}816+817+func shiftChannel(ch uint8, delta int) uint8 {818+ v := int(ch) + delta819+ if v < 0 {820+ return 0821+ }822+ if v > 255 {823+ return 255824+ }825+ return uint8(v)826+}827+828+func coverStreamRNG(seed uint64, stream string) *rand.Rand {829+ h := fnv.New64a()830+ var raw [8]byte831+ binary.LittleEndian.PutUint64(raw[:], seed)832+ _, _ = h.Write(raw[:])833+ _, _ = h.Write([]byte(stream))834+ return rand.New(rand.NewSource(int64(h.Sum64())))835+}836+837+func coverSeedHash(text string) uint64 {838+ h := fnv.New64a()839+ h.Write([]byte(text))840+ return h.Sum64()841+}
A pdfexport/cover_art_test.go +89 -0
@@ -0,0 +1,89 @@
1+package pdfexport2+3+import (4+ "bytes"5+ "image"6+ "image/png"7+ "math"8+ "testing"9+)10+11+// --- Cover art seeding (P1) ---12+13+func TestRenderCoverArtworkPNGDeterministic(t *testing.T) {14+ base := RGB{R: 86, G: 124, B: 149}15+ seed := "quiet-fjord"16+17+ first := renderCoverArtworkPNG(seed, "front", base)18+ second := renderCoverArtworkPNG(seed, "front", base)19+20+ if len(first) == 0 || len(second) == 0 {21+ t.Fatalf("renderCoverArtworkPNG returned empty output")22+ }23+ if !bytes.Equal(first, second) {24+ t.Fatalf("expected identical PNG bytes for identical seed and variant")25+ }26+}27+28+func TestRenderCoverArtworkPNGVariesBySeedAndVariant(t *testing.T) {29+ if testing.Short() {30+ t.Skip("skipping cover art variation check in short mode")31+ }32+33+ base := RGB{R: 90, G: 128, B: 154}34+ frontA := renderCoverArtworkPNG("quiet-fjord", "front", base)35+ frontB := renderCoverArtworkPNG("quiet-fjord-alt", "front", base)36+ backA := renderCoverArtworkPNG("quiet-fjord", "back", base)37+38+ assertArtworkDiff(t, frontA, frontB, 0.03)39+ assertArtworkDiff(t, frontA, backA, 0.03)40+}41+42+func assertArtworkDiff(t *testing.T, left, right []byte, minMeanDiff float64) {43+ t.Helper()44+45+ if bytes.Equal(left, right) {46+ t.Fatalf("unexpected byte-identical images for different inputs")47+ }48+49+ imgA := decodePNG(t, left)50+ imgB := decodePNG(t, right)51+ diff := sampledMeanChannelDiff(imgA, imgB, 12)52+ if diff < minMeanDiff {53+ t.Fatalf("mean sampled channel diff too low: got %.4f want >= %.4f", diff, minMeanDiff)54+ }55+}56+57+func decodePNG(t *testing.T, data []byte) image.Image {58+ t.Helper()59+ img, err := png.Decode(bytes.NewReader(data))60+ if err != nil {61+ t.Fatalf("png.Decode error: %v", err)62+ }63+ return img64+}65+66+func sampledMeanChannelDiff(a, b image.Image, step int) float64 {67+ bounds := a.Bounds()68+ if step < 1 {69+ step = 170+ }71+72+ var total float6473+ var samples int74+ for y := bounds.Min.Y; y < bounds.Max.Y; y += step {75+ for x := bounds.Min.X; x < bounds.Max.X; x += step {76+ ar, ag, ab, _ := a.At(x, y).RGBA()77+ br, bg, bb, _ := b.At(x, y).RGBA()78+ total += math.Abs(float64(ar)-float64(br)) / 6553579+ total += math.Abs(float64(ag)-float64(bg)) / 6553580+ total += math.Abs(float64(ab)-float64(bb)) / 6553581+ samples += 382+ }83+ }84+85+ if samples == 0 {86+ return 087+ }88+ return total / float64(samples)89+}
M pdfexport/jsonl.go +13 -1
M pdfexport/jsonl_test.go +98 -0
@@ -47,6 +47,39 @@ func TestParseJSONLFile(t *testing.T) {
4747 }4848 }4949 50+func TestParseJSONLFileHydratesNonogramFromSaveWithoutSnippet(t *testing.T) {51+ path := filepath.Join(t.TempDir(), "nonogram-save-only.jsonl")52+ record := JSONLRecord{53+ Schema: ExportSchemaV1,54+ Pack: JSONLPackMeta{55+ Generated: "2026-02-22T10:00:00Z",56+ Version: "v-test",57+ Category: "Nonogram",58+ ModeSelection: "Mini",59+ Count: 1,60+ },61+ Puzzle: JSONLPuzzle{62+ Index: 1,63+ Name: "fern-owl",64+ Game: "Nonogram",65+ Mode: "Mini",66+ Save: json.RawMessage(`{"width":2,"height":2,"row-hints":[[1],[1]],"col-hints":[[1],[1]],"state":" \n "}`),67+ },68+ }69+ writeSingleJSONLRecord(t, path, record)70+71+ doc, err := ParseJSONLFile(path)72+ if err != nil {73+ t.Fatal(err)74+ }75+ if got, want := len(doc.Puzzles), 1; got != want {76+ t.Fatalf("puzzles = %d, want %d", got, want)77+ }78+ if doc.Puzzles[0].Nonogram == nil {79+ t.Fatal("expected nonogram print payload from save hydration")80+ }81+}82+5083 func TestParseJSONLFileRejectsNonJSONLExtension(t *testing.T) {5184 path := filepath.Join(t.TempDir(), "pack.md")5285 if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
@@ -282,6 +315,71 @@ func TestParseJSONLFileHydratesHashiFromSave(t *testing.T) {
282315 }283316 }284317 318+func TestParseJSONLFileIgnoresMalformedSnippetWhenSaveHydrated(t *testing.T) {319+ path := filepath.Join(t.TempDir(), "sudoku-malformed-snippet.jsonl")320+ record := JSONLRecord{321+ Schema: ExportSchemaV1,322+ Pack: JSONLPackMeta{323+ Generated: "2026-02-22T10:00:00Z",324+ Version: "v-test",325+ Category: "Sudoku",326+ ModeSelection: "Easy",327+ Count: 1,328+ },329+ Puzzle: JSONLPuzzle{330+ Index: 1,331+ Name: "sage-briar",332+ Game: "Sudoku",333+ Mode: "Easy",334+ Save: json.RawMessage(`{"provided":[{"x":0,"y":0,"v":5}]}`),335+ Snippet: "| bad |\n",336+ },337+ }338+ writeSingleJSONLRecord(t, path, record)339+340+ doc, err := ParseJSONLFile(path)341+ if err != nil {342+ t.Fatalf("expected lenient parse when save hydration succeeds, got: %v", err)343+ }344+ if got, want := len(doc.Puzzles), 1; got != want {345+ t.Fatalf("puzzles = %d, want %d", got, want)346+ }347+ if doc.Puzzles[0].Sudoku == nil {348+ t.Fatal("expected sudoku print payload from save hydration")349+ }350+}351+352+func TestParseJSONLFileFailsMalformedSnippetWithoutRenderablePayload(t *testing.T) {353+ path := filepath.Join(t.TempDir(), "lights-malformed-snippet.jsonl")354+ record := JSONLRecord{355+ Schema: ExportSchemaV1,356+ Pack: JSONLPackMeta{357+ Generated: "2026-02-22T10:00:00Z",358+ Version: "v-test",359+ Category: "Lights Out",360+ ModeSelection: "Standard",361+ Count: 1,362+ },363+ Puzzle: JSONLPuzzle{364+ Index: 1,365+ Name: "glow-shore",366+ Game: "Lights Out",367+ Mode: "Standard",368+ Save: json.RawMessage(`{"size":5}`),369+ Snippet: "| bad |\n",370+ },371+ }372+ writeSingleJSONLRecord(t, path, record)373+374+ _, err := ParseJSONLFile(path)375+ if err == nil {376+ t.Fatal("expected parse error when neither save nor snippet produces renderable payload")377+ }378+ if !strings.Contains(err.Error(), "parse printable snippet") {379+ t.Fatalf("unexpected error: %v", err)380+ }381+}382+285383 func writeSingleJSONLRecord(t *testing.T, path string, record JSONLRecord) {286384 t.Helper()287385 data, err := json.Marshal(record)
M pdfexport/printdata.go +130 -0
@@ -15,6 +15,14 @@ type nurikabeSave struct {
1515 Clues string `json:"clues"`1616 }1717 18+type nonogramSave struct {19+ State string `json:"state"`20+ Width int `json:"width"`21+ Height int `json:"height"`22+ RowHints [][]int `json:"row-hints"`23+ ColHints [][]int `json:"col-hints"`24+}25+1826 type hashiSave struct {1927 Width int `json:"width"`2028 Height int `json:"height"`
@@ -71,6 +79,47 @@ type wordSearchWord struct {
7179 Text string `json:"text"`7280 }7381 82+func ParseNonogramPrintData(saveData []byte) (*NonogramData, error) {83+ if len(strings.TrimSpace(string(saveData))) == 0 {84+ return nil, nil85+ }86+87+ var save nonogramSave88+ if err := json.Unmarshal(saveData, &save); err != nil {89+ return nil, fmt.Errorf("decode nonogram save: %w", err)90+ }91+92+ stateRows := splitNonogramStateRows(save.State)93+94+ width := save.Width95+ if width <= 0 {96+ width = len(save.ColHints)97+ }98+ if width <= 0 {99+ width = maxRuneWidth(stateRows)100+ }101+102+ height := save.Height103+ if height <= 0 {104+ height = len(save.RowHints)105+ }106+ if height <= 0 {107+ height = len(stateRows)108+ }109+110+ if width <= 0 || height <= 0 {111+ return nil, nil112+ }113+114+ return &NonogramData{115+ Width: width,116+ Height: height,117+ RowHints: normalizeNonogramHintRows(save.RowHints, height),118+ ColHints: normalizeNonogramHintRows(save.ColHints, width),119+ Grid: normalizeNonogramStateGrid(stateRows, width, height),120+ }, nil121+}122+74123 func ParseNurikabePrintData(saveData []byte) (*NurikabeData, error) {75124 if len(strings.TrimSpace(string(saveData))) == 0 {76125 return nil, nil
@@ -356,6 +405,14 @@ func hydratePuzzlePrintData(p *Puzzle) {
356405 }357406 358407 switch normalizePrintableCategory(p.Category) {408+ case "nonogram":409+ if p.Nonogram != nil {410+ return411+ }412+ nonogram, err := ParseNonogramPrintData(p.SaveData)413+ if err == nil {414+ p.Nonogram = nonogram415+ }359416 case "hashiwokakero":360417 if p.Hashi != nil {361418 return
@@ -517,3 +574,76 @@ func parseNurikabeClues(raw string, width, height int) ([][]int, error) {
517574 518575 return clues, nil519576 }577+578+func splitNonogramStateRows(raw string) []string {579+ normalized := strings.ReplaceAll(strings.ReplaceAll(raw, "\r\n", "\n"), "\r", "\n")580+ if normalized == "" {581+ return nil582+ }583+ return strings.Split(normalized, "\n")584+}585+586+func maxRuneWidth(rows []string) int {587+ maxWidth := 0588+ for _, row := range rows {589+ if n := len([]rune(row)); n > maxWidth {590+ maxWidth = n591+ }592+ }593+ return maxWidth594+}595+596+func normalizeNonogramHintRows(src [][]int, size int) [][]int {597+ if size <= 0 {598+ return nil599+ }600+601+ normalized := make([][]int, size)602+ for i := 0; i < size; i++ {603+ if i >= len(src) {604+ normalized[i] = []int{0}605+ continue606+ }607+608+ filtered := make([]int, 0, len(src[i]))609+ for _, value := range src[i] {610+ if value > 0 {611+ filtered = append(filtered, value)612+ }613+ }614+ if len(filtered) == 0 {615+ filtered = []int{0}616+ }617+ normalized[i] = filtered618+ }619+620+ return normalized621+}622+623+func normalizeNonogramStateGrid(rows []string, width, height int) [][]string {624+ if width <= 0 || height <= 0 {625+ return nil626+ }627+628+ grid := make([][]string, height)629+ for y := 0; y < height; y++ {630+ grid[y] = make([]string, width)631+ for x := 0; x < width; x++ {632+ grid[y][x] = " "633+ }634+635+ if y >= len(rows) {636+ continue637+ }638+639+ runes := []rune(rows[y])640+ for x := 0; x < width && x < len(runes); x++ {641+ if runes[x] == ' ' {642+ continue643+ }644+ grid[y][x] = string(runes[x])645+ }646+ }647+648+ return grid649+}
M pdfexport/printdata_test.go +51 -0
@@ -2,6 +2,39 @@ package pdfexport
22 33 import "testing"44 5+func TestParseNonogramPrintData(t *testing.T) {6+ save := []byte(`{"state":" .-\n.. ","row-hints":[[1],[2]],"col-hints":[[2],[1],[1]]}`)7+8+ data, err := ParseNonogramPrintData(save)9+ if err != nil {10+ t.Fatal(err)11+ }12+ if data == nil {13+ t.Fatal("expected nonogram print data")14+ }15+ if got, want := data.Width, 3; got != want {16+ t.Fatalf("width = %d, want %d", got, want)17+ }18+ if got, want := data.Height, 2; got != want {19+ t.Fatalf("height = %d, want %d", got, want)20+ }21+ if got, want := data.RowHints[0][0], 1; got != want {22+ t.Fatalf("row hint[0][0] = %d, want %d", got, want)23+ }24+ if got, want := data.ColHints[0][0], 2; got != want {25+ t.Fatalf("col hint[0][0] = %d, want %d", got, want)26+ }27+ if got, want := data.Grid[0][0], " "; got != want {28+ t.Fatalf("grid[0][0] = %q, want %q", got, want)29+ }30+ if got, want := data.Grid[0][1], "."; got != want {31+ t.Fatalf("grid[0][1] = %q, want %q", got, want)32+ }33+ if got, want := data.Grid[0][2], "-"; got != want {34+ t.Fatalf("grid[0][2] = %q, want %q", got, want)35+ }36+}37+538 func TestParseNurikabePrintData(t *testing.T) {639 save := []byte(`{"width":3,"height":2,"clues":"1,0,2\n0,3,0","marks":"???\n???"}`)740
@@ -167,6 +200,24 @@ func TestHydratePuzzlePrintDataForTakuzu(t *testing.T) {
167200 }168201 }169202 203+func TestHydratePuzzlePrintDataForNonogram(t *testing.T) {204+ p := Puzzle{205+ Category: "Nonogram",206+ SaveData: []byte(`{"width":2,"height":2,"row-hints":[[1],[1]],"col-hints":[[1],[1]],"state":" \n "}`),207+ }208+209+ hydratePuzzlePrintData(&p)210+ if p.Nonogram == nil {211+ t.Fatal("expected nonogram payload from save hydration")212+ }213+ if got, want := p.Nonogram.Width, 2; got != want {214+ t.Fatalf("width = %d, want %d", got, want)215+ }216+ if got, want := p.Nonogram.Height, 2; got != want {217+ t.Fatalf("height = %d, want %d", got, want)218+ }219+}220+170221 func TestHydratePuzzlePrintDataForNurikabeAndShikaku(t *testing.T) {171222 nurikabePuzzle := Puzzle{172223 Category: "Nurikabe",
M pdfexport/render.go +78 -45
@@ -116,6 +116,7 @@ func renderTitlePage(pdf *fpdf.Fpdf, docs []PackDocument, puzzles []Puzzle, cfg
116116 pdf.AddPage()117117 pageW, pageH := pdf.GetPageSize()118118 margin := 12.0119+ contentWidth := pageW - 2*margin119120 120121 pdf.SetTextColor(20, 20, 20)121122 pdf.SetFont(sansFontFamily, "B", 22)
@@ -132,56 +133,109 @@ func renderTitlePage(pdf *fpdf.Fpdf, docs []PackDocument, puzzles []Puzzle, cfg
132133 pdf.SetXY(0, 44)133134 pdf.CellFormat(pageW, 6, "PuzzleTea Puzzle Pack", "", 0, "C", false, 0, "")134135 136+ versionLine := fmt.Sprintf("PuzzleTea Version: %s", strings.Join(summarizeVersions(docs), ", "))137+ pdf.SetFont(sansFontFamily, "", 10)138+ wrappedVersions := pdf.SplitLines([]byte(versionLine), contentWidth)139+ if len(wrappedVersions) == 0 {140+ wrappedVersions = [][]byte{[]byte(versionLine)}141+ }142+143+ headerLineH := 4.8144+ headerGap := 1.2145+ headerStartY := 54.8135146 metaY := 56.0147+ if header := strings.TrimSpace(cfg.HeaderText); header != "" {148+ pdf.SetFont(sansFontFamily, "", 9.2)149+ pdf.SetTextColor(74, 74, 74)150+ wrappedHeader := pdf.SplitLines([]byte(header), contentWidth)151+ if len(wrappedHeader) == 0 {152+ wrappedHeader = [][]byte{[]byte(header)}153+ }154+155+ metaY = headerStartY + float64(len(wrappedHeader))*headerLineH + headerGap156+ sourceStartY := titlePageSourceTableStartY(metaY, len(wrappedVersions))157+ sourceMaxY := pageH - 45158+ if spare := titlePageSourceTableWhitespace(sourceMaxY, sourceStartY, len(docs)); spare > 0 {159+ headerGap += spare160+ }161+162+ headerY := headerStartY163+ for _, line := range wrappedHeader {164+ pdf.SetXY(margin, headerY)165+ pdf.CellFormat(contentWidth, headerLineH, string(line), "", 0, "C", false, 0, "")166+ headerY += headerLineH167+ }168+ metaY = headerY + headerGap169+ }170+136171 pdf.SetTextColor(25, 25, 25)137172 pdf.SetFont(sansFontFamily, "", 10)138173 pdf.SetXY(margin, metaY)139- pdf.CellFormat(pageW-2*margin, 6, fmt.Sprintf("Generated: %s", cfg.GeneratedAt.Format("January 2, 2006")), "", 0, "L", false, 0, "")174+ pdf.CellFormat(contentWidth, 6, fmt.Sprintf("Generated: %s", cfg.GeneratedAt.Format("January 2, 2006")), "", 0, "L", false, 0, "")140175 metaY += 6141- versionLine := fmt.Sprintf("PuzzleTea Version: %s", strings.Join(summarizeVersions(docs), ", "))142- wrappedVersions := pdf.SplitLines([]byte(versionLine), pageW-2*margin)143- if len(wrappedVersions) == 0 {144- wrappedVersions = [][]byte{[]byte(versionLine)}145- }146176 for _, line := range wrappedVersions {147177 pdf.SetXY(margin, metaY)148- pdf.CellFormat(pageW-2*margin, 5.2, string(line), "", 0, "L", false, 0, "")178+ pdf.CellFormat(contentWidth, 5.2, string(line), "", 0, "L", false, 0, "")149179 metaY += 5.2150180 }151181 metaY += 0.8152182 153- categories := summarizeCategories(puzzles)154183 pdf.SetXY(margin, metaY)155- pdf.CellFormat(pageW-2*margin, 6, fmt.Sprintf("Puzzles: %d", len(puzzles)), "", 0, "L", false, 0, "")184+ pdf.CellFormat(contentWidth, 6, fmt.Sprintf("Puzzles: %d", len(puzzles)), "", 0, "L", false, 0, "")156185 metaY += 6157- categoryLine := fmt.Sprintf("Categories: %s", strings.Join(categories, ", "))158- wrappedCategories := pdf.SplitLines([]byte(categoryLine), pageW-2*margin)159- if len(wrappedCategories) == 0 {160- wrappedCategories = [][]byte{[]byte(categoryLine)}161- }162- for _, line := range wrappedCategories {163- pdf.SetXY(margin, metaY)164- pdf.CellFormat(pageW-2*margin, 5.2, string(line), "", 0, "L", false, 0, "")165- metaY += 5.2166- }167- metaY += 2.8186+ metaY += 1.8168187 169188 pdf.SetFont(sansFontFamily, "B", 10)170189 pdf.SetTextColor(45, 45, 45)171190 pdf.SetXY(margin, metaY)172- pdf.CellFormat(pageW-2*margin, 6, "Source Exports", "", 0, "L", false, 0, "")191+ pdf.CellFormat(contentWidth, 6, "Source Exports", "", 0, "L", false, 0, "")173192 metaY += 7174193 175- renderSourceExportsTable(pdf, docs, margin, metaY, pageW-2*margin, pageH-45)194+ renderSourceExportsTable(pdf, docs, margin, metaY, contentWidth, pageH-45)176195 177196 pdf.SetTextColor(50, 50, 50)178197 pdf.SetFont(sansFontFamily, "B", 12)179198 pdf.SetXY(margin, pageH-30)180- pdf.CellFormat(pageW-2*margin, 7, "Made with PuzzleTea", "", 0, "C", false, 0, "")199+ pdf.CellFormat(contentWidth, 7, "Made with PuzzleTea", "", 0, "C", false, 0, "")181200 182201 pdf.SetFont(sansFontFamily, "", 10)183202 pdf.SetXY(margin, pageH-22)184- pdf.CellFormat(pageW-2*margin, 6, cfg.AdvertText, "", 0, "C", false, 0, "")203+ pdf.CellFormat(contentWidth, 6, cfg.AdvertText, "", 0, "C", false, 0, "")204+}205+206+func titlePageSourceTableStartY(metaY float64, versionLineCount int) float64 {207+ y := metaY208+ y += 6209+ y += float64(max(versionLineCount, 1)) * 5.2210+ y += 0.8211+ y += 6212+ y += 1.8213+ y += 7214+ return y215+}216+217+func titlePageSourceTableWhitespace(maxY, sourceStartY float64, docCount int) float64 {218+ const (219+ headerHeight = 5.2220+ rowHeight = 4.8221+ )222+223+ availableRowsHeight := maxY - sourceStartY - headerHeight224+ if availableRowsHeight < rowHeight {225+ return 0226+ }227+228+ maxRows := int(math.Floor(availableRowsHeight / rowHeight))229+ if maxRows < 1 {230+ return 0231+ }232+233+ rowCount := min(docCount, maxRows)234+ used := headerHeight + float64(rowCount)*rowHeight235+ if spare := maxY - sourceStartY - used; spare > 0 {236+ return spare237+ }238+ return 0185239 }186240 187241 func renderPuzzlePage(pdf *fpdf.Fpdf, puzzle Puzzle) {
@@ -961,27 +1015,6 @@ func drawNonogramMajorLines(
9611015 }9621016 }9631017 964-func summarizeCategories(puzzles []Puzzle) []string {965- set := map[string]struct{}{}966- for _, p := range puzzles {967- category := strings.TrimSpace(p.Category)968- if category == "" {969- continue970- }971- set[category] = struct{}{}972- }973-974- categories := make([]string, 0, len(set))975- for category := range set {976- categories = append(categories, category)977- }978- sort.Strings(categories)979- if len(categories) == 0 {980- return []string{"Unknown"}981- }982- return categories983-}984-9851018 func summarizeVersions(docs []PackDocument) []string {9861019 set := map[string]struct{}{}9871020 for _, doc := range docs {
M pdfexport/render_cover.go +15 -197
@@ -3,7 +3,6 @@ package pdfexport
33 import (44 "fmt"55 "hash/fnv"6- "math"76 "math/rand"87 "strings"98
@@ -82,7 +81,7 @@ func renderCoverPage(pdf *fpdf.Fpdf, _ []Puzzle, cfg RenderConfig) {
8281 drawCoverFrame(pdf, frameInset, pageW, pageH, ink)8382 8483 scene := rectMM{x: frameInset + 4.0, y: frameInset + 10.0, w: pageW - (frameInset+4.0)*2, h: 132.0}85- drawWoodcutScene(pdf, scene, coverColor, ink, cfg.ShuffleSeed)84+ drawCoverArtwork(pdf, scene, cfg.ShuffleSeed, coverColor, ink)8685 8786 subtitle := strings.TrimSpace(cfg.CoverSubtitle)8887 if subtitle == "" {
@@ -129,197 +128,16 @@ func drawCoverFrame(pdf *fpdf.Fpdf, inset, pageW, pageH float64, ink RGB) {
129128 pdf.Rect(inner, inner, pageW-2*inner, pageH-2*inner, "D")130129 }131130 132-func drawWoodcutScene(pdf *fpdf.Fpdf, scene rectMM, bg, ink RGB, seed string) {133- drawStippleTexture(pdf, scene, ink, seed)134- drawHatchingBands(pdf, scene, ink)135- drawPineForestMass(pdf, scene, ink)136- drawIsometricRuins(pdf, scene, bg, ink)137- drawSkullMotifs(pdf, scene, bg, ink)138-}139-140-func drawStippleTexture(pdf *fpdf.Fpdf, scene rectMM, ink RGB, seed string) {141- h := fnv.New64a()142- h.Write([]byte(strings.TrimSpace(seed) + "-stipple"))143- rng := rand.New(rand.NewSource(int64(h.Sum64())))144-145- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))146- for x := scene.x + 1.0; x < scene.x+scene.w-1.0; x += 3.1 {147- for y := scene.y + 2.0; y < scene.y+scene.h*0.58; y += 3.2 {148- if rng.Float64() < 0.22 {149- jitterX := (rng.Float64() - 0.5) * 0.9150- jitterY := (rng.Float64() - 0.5) * 0.9151- pdf.Circle(x+jitterX, y+jitterY, 0.11, "F")152- }153- }154- }155-}156-157-func drawHatchingBands(pdf *fpdf.Fpdf, scene rectMM, ink RGB) {158- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))159- pdf.SetLineWidth(0.28)160-161- baseY := scene.y + scene.h*0.64162- for i := -8; i < 34; i++ {163- x0 := scene.x + float64(i)*4.4164- y0 := baseY165- x1 := x0 + scene.h*0.55166- y1 := baseY + scene.h*0.30167- pdf.Line(x0, y0, x1, y1)168- }169-170- for i := -8; i < 30; i++ {171- x0 := scene.x + float64(i)*5.0172- y0 := baseY + 7.5173- x1 := x0 + scene.h*0.48174- y1 := baseY + scene.h*0.33175- pdf.Line(x0, y0, x1, y1)176- }177-}131+func drawCoverArtwork(pdf *fpdf.Fpdf, scene rectMM, seed string, bg, ink RGB) {132+ drawCoverArtworkImage(pdf, scene, seed, "front", bg)178133 179-func drawPineForestMass(pdf *fpdf.Fpdf, scene rectMM, ink RGB) {180- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))181134 pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))135+ pdf.SetLineWidth(0.40)136+ pdf.Rect(scene.x, scene.y, scene.w, scene.h, "D")182137 183- layers := []struct {184- y float64185- count int186- w float64187- h float64188- offset float64189- }{190- {y: scene.y + scene.h*0.45, count: 22, w: 4.4, h: 13.0, offset: 0.8},191- {y: scene.y + scene.h*0.53, count: 19, w: 5.4, h: 16.0, offset: 1.4},192- {y: scene.y + scene.h*0.62, count: 16, w: 6.2, h: 18.4, offset: 2.0},193- }194-195- for _, layer := range layers {196- step := scene.w / float64(layer.count)197- for i := 0; i < layer.count; i++ {198- cx := scene.x + float64(i)*step + layer.offset199- pts := []fpdf.PointType{200- {X: cx, Y: layer.y - layer.h},201- {X: cx - layer.w, Y: layer.y},202- {X: cx + layer.w, Y: layer.y},203- }204- pdf.Polygon(pts, "F")205- pdf.SetLineWidth(0.22)206- pdf.Line(cx, layer.y, cx, layer.y+2.4)207- }208- }209-}210-211-func drawIsometricRuins(pdf *fpdf.Fpdf, scene rectMM, bg, ink RGB) {212- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))213- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))214-215- centerX := scene.x + scene.w*0.52216- baseY := scene.y + scene.h*0.80217- drawIsometricGrid(pdf, centerX-44, baseY-17, 88, 34, ink)218-219- drawStoneArch(pdf, centerX-22, baseY-35, 25, 27, 8, bg, ink)220- drawStoneArch(pdf, centerX+8, baseY-48, 20, 23, 7, bg, ink)221-222- drawRubbleBlock(pdf, centerX-39, baseY-6, 9, 5, 5, ink)223- drawRubbleBlock(pdf, centerX+31, baseY-3, 10, 5, 6, ink)224- drawRubbleBlock(pdf, centerX+12, baseY+5, 8, 4, 4, ink)225-}226-227-func drawIsometricGrid(pdf *fpdf.Fpdf, x, y, w, h float64, ink RGB) {228- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))229- pdf.SetLineWidth(0.26)230-231- for i := -8; i < 14; i++ {232- sx := x + float64(i)*4233- pdf.Line(sx, y, sx+w*0.5, y+h*0.5)234- }235- for i := -8; i < 14; i++ {236- sx := x + w + float64(i)*4237- pdf.Line(sx, y, sx-w*0.5, y+h*0.5)238- }239-}240-241-func drawStoneArch(pdf *fpdf.Fpdf, x, y, w, h, depth float64, bg, ink RGB) {242- frontLeft := []fpdf.PointType{{X: x, Y: y}, {X: x + w*0.18, Y: y - depth}, {X: x + w*0.18, Y: y + h - depth}, {X: x, Y: y + h}}243- frontRight := []fpdf.PointType{{X: x + w*0.82, Y: y - depth}, {X: x + w, Y: y}, {X: x + w, Y: y + h}, {X: x + w*0.82, Y: y + h - depth}}244-245- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))246- pdf.Polygon(frontLeft, "F")247- pdf.Polygon(frontRight, "F")248-249- outerR := w * 0.50250- innerR := w * 0.33251- cx := x + w*0.5252- cy := y + h*0.42253- drawArcPolyline(pdf, cx, cy, outerR, math.Pi, 2*math.Pi, 24)254-255- pdf.SetDrawColor(int(bg.R), int(bg.G), int(bg.B))256- pdf.SetLineWidth(2.0)257- drawArcPolyline(pdf, cx, cy, innerR, math.Pi, 2*math.Pi, 24)258- pdf.Line(x+w*0.33, y+h, x+w*0.33, cy)259- pdf.Line(x+w*0.67, y+h-depth*0.2, x+w*0.67, cy)260-261- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))262- pdf.SetLineWidth(0.28)263- for i := 0; i < 6; i++ {264- sy := y + 3 + float64(i)*4.2265- pdf.Line(x+1.5, sy, x+w*0.16, sy-0.6)266- pdf.Line(x+w*0.84, sy-0.5, x+w-1.5, sy+0.4)267- }268-}269-270-func drawArcPolyline(pdf *fpdf.Fpdf, cx, cy, r, start, end float64, segments int) {271- if segments < 2 {272- segments = 2273- }274- step := (end - start) / float64(segments)275- px := cx + math.Cos(start)*r276- py := cy + math.Sin(start)*r277- for i := 1; i <= segments; i++ {278- a := start + float64(i)*step279- x := cx + math.Cos(a)*r280- y := cy + math.Sin(a)*r281- pdf.Line(px, py, x, y)282- px, py = x, y283- }284-}285-286-func drawRubbleBlock(pdf *fpdf.Fpdf, x, y, w, h, skew float64, ink RGB) {287- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))288- pts := []fpdf.PointType{289- {X: x, Y: y},290- {X: x + w, Y: y - skew*0.2},291- {X: x + w + skew, Y: y + h},292- {X: x + skew, Y: y + h + skew*0.2},293- }294- pdf.Polygon(pts, "F")295-}296-297-func drawSkullMotifs(pdf *fpdf.Fpdf, scene rectMM, bg, ink RGB) {298- centers := []struct {299- x float64300- y float64301- r float64302- }{303- {x: scene.x + scene.w*0.30, y: scene.y + scene.h*0.84, r: 2.9},304- {x: scene.x + scene.w*0.69, y: scene.y + scene.h*0.87, r: 2.4},305- }306-307- for _, c := range centers {308- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))309- pdf.SetFillColor(int(ink.R), int(ink.G), int(ink.B))310- pdf.Circle(c.x, c.y, c.r, "F")311-312- pdf.SetFillColor(int(bg.R), int(bg.G), int(bg.B))313- pdf.Circle(c.x-c.r*0.35, c.y-c.r*0.1, c.r*0.22, "F")314- pdf.Circle(c.x+c.r*0.35, c.y-c.r*0.08, c.r*0.20, "F")315- jaw := []fpdf.PointType{{X: c.x - c.r*0.48, Y: c.y + c.r*0.2}, {X: c.x + c.r*0.48, Y: c.y + c.r*0.2}, {X: c.x, Y: c.y + c.r*0.72}}316- pdf.Polygon(jaw, "F")317-318- pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))319- pdf.SetLineWidth(0.22)320- pdf.Line(c.x-c.r*0.6, c.y-c.r*0.85, c.x-c.r*1.15, c.y-c.r*1.45)321- pdf.Line(c.x+c.r*0.5, c.y-c.r*0.7, c.x+c.r*1.0, c.y-c.r*1.3)322- }138+ pdf.SetLineWidth(0.20)139+ inset := 1.8140+ pdf.Rect(scene.x+inset, scene.y+inset, scene.w-2*inset, scene.h-2*inset, "D")323141 }324142 325143 func renderBackCoverPage(pdf *fpdf.Fpdf, cfg RenderConfig, coverColor RGB) {
@@ -334,7 +152,7 @@ func renderBackCoverPage(pdf *fpdf.Fpdf, cfg RenderConfig, coverColor RGB) {
334152 drawCoverFrame(pdf, frameInset, pageW, pageH, ink)335153 336154 motif := rectMM{x: frameInset + 5.5, y: frameInset + 14.0, w: pageW - 2*(frameInset+5.5), h: 96.0}337- drawBackMotif(pdf, motif, coverColor, ink)155+ drawBackMotif(pdf, motif, cfg.ShuffleSeed, coverColor, ink)338156 339157 labelW := pageW - 2*(frameInset+6.0)340158 pdf.SetTextColor(int(ink.R), int(ink.G), int(ink.B))
@@ -351,10 +169,10 @@ func renderBackCoverPage(pdf *fpdf.Fpdf, cfg RenderConfig, coverColor RGB) {
351169 pdf.CellFormat(labelW, 4.2, fmt.Sprintf("VOL. %02d", cfg.VolumeNumber), "", 0, "L", false, 0, "")352170 }353171 354-func drawBackMotif(pdf *fpdf.Fpdf, scene rectMM, bg, ink RGB) {355- drawStippleTexture(pdf, scene, ink, "back-cover")356- drawHatchingBands(pdf, scene, ink)357- drawPineForestMass(pdf, scene, ink)358- drawStoneArch(pdf, scene.x+scene.w*0.58, scene.y+scene.h*0.56, 22, 24, 7, bg, ink)359- drawRubbleBlock(pdf, scene.x+scene.w*0.28, scene.y+scene.h*0.72, 8, 4, 4, ink)172+func drawBackMotif(pdf *fpdf.Fpdf, scene rectMM, seed string, bg, ink RGB) {173+ drawCoverArtworkImage(pdf, scene, seed, "back", bg)174+175+ pdf.SetDrawColor(int(ink.R), int(ink.G), int(ink.B))176+ pdf.SetLineWidth(0.34)177+ pdf.Rect(scene.x, scene.y, scene.w, scene.h, "D")360178 }
M pdfexport/types.go +1 -0