typstcard/format.py

243 lines
6.3 KiB
Python
Raw Normal View History

2023-09-24 00:28:46 +00:00
#!/usr/bin/env python3
import argparse
2023-09-24 05:06:59 +00:00
import base64
2023-09-24 00:28:46 +00:00
import csv
import hashlib
2023-09-24 00:28:46 +00:00
import json
import os
import string
import typing
import urllib.parse
2023-09-24 00:28:46 +00:00
import xml.etree.ElementTree as ET
import requests
import imb
2023-10-01 02:13:40 +00:00
# A lot of stuff needs to be in the same directory, just chdir
os.chdir(os.path.dirname(os.path.realpath(__file__)))
2023-09-24 00:28:46 +00:00
def iso_code(s: str) -> str:
if len(s) != 2:
raise ValueError("must be 2 characters long")
s = s.lower()
if not (s[0] in string.ascii_lowercase and s[1] in string.ascii_lowercase):
raise ValueError("must be ascii letters")
return s
def get_discord_avatar(
url: urllib.parse.ParseResult, secrets: dict
) -> (typing.Optional[str], str):
try:
token = secrets["discord_token"]
user_info = requests.get(
f"https://discord.com/api/users/{url.path}",
headers={"Authorization": f"Bot {token}"},
).json()
avatar_hash = user_info["avatar"]
return (
f"https://cdn.discordapp.com/avatars/{url.path}/{avatar_hash}.png?size=4096",
"png",
)
except KeyError:
return None, ""
def get_fedi_avatar(
url: urllib.parse.ParseResult, secrets: dict
) -> (typing.Optional[str], str):
try:
mastodon_api = secrets["mastodon_api"]
user_info = requests.get(
f"{mastodon_api}/api/v1/accounts/lookup", params={"acct": url.path}
).json()
avatar_url = user_info["avatar_static"]
extension = avatar_url.split(".")[-1]
return avatar_url, extension
except KeyError:
return None, ""
def get_orig_avatar(url: str, basename: str, secrets: dict) -> typing.Optional[bytes]:
2023-09-24 00:28:46 +00:00
if not os.path.exists("cache"):
os.mkdir("cache")
url_parts = urllib.parse.urlparse(url)
if url_parts.scheme == "fedi":
real_url, extension = get_fedi_avatar(url_parts, secrets)
elif url_parts.scheme == "discord":
real_url, extension = get_discord_avatar(url_parts, secrets)
else:
real_url = url
extension = url_parts.path.rsplit(".", 1)[1]
if real_url is None:
return None
img_name = f"cache/{basename}.{extension}"
if os.path.exists(img_name):
with open(img_name, "rb") as infile:
2023-09-24 05:06:59 +00:00
return infile.read()
result = requests.get(real_url)
2023-09-24 00:28:46 +00:00
if result.ok:
with open(img_name, "wb") as outfile:
2023-09-24 00:28:46 +00:00
outfile.write(result.content)
2023-09-24 05:06:59 +00:00
return result.content
return None
def get_avatar(url: str, secrets: dict) -> str:
basename = hashlib.sha256(url.encode("utf-8")).hexdigest()
if os.path.exists(f"cache/{basename}.svg"):
return f"cache/{basename}.svg"
avatar_raster = get_orig_avatar(url, basename, secrets)
2023-09-24 05:06:59 +00:00
if avatar_raster is None:
return ""
2023-10-01 02:13:40 +00:00
svg_text = f"""<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<clipPath id="circle">
2023-10-01 02:13:40 +00:00
<circle cx="256" cy="256" r="256" />
</clipPath>
2023-10-01 02:13:40 +00:00
<image width="512" height="512" clip-path="url(#circle)"
xlink:href="data:;base64,{base64.b64encode(avatar_raster).decode("utf-8")}" />
2023-09-24 05:06:59 +00:00
</svg>"""
with open(f"cache/{basename}.svg", "w") as svgfile:
2023-09-24 05:06:59 +00:00
svgfile.write(svg_text)
return f"cache/{basename}.svg"
2023-09-24 00:28:46 +00:00
def get_country_name(
root: ET.ElementTree, destination: str, alt=None
) -> typing.Optional[str]:
elements = root.findall(
f"./localeDisplayNames/territories/territory[@type='{destination.upper()}']"
)
normal = None
for element in elements:
if element.attrib.get("alt") == alt:
return element.text
elif element.attrib.get("alt") is None:
normal = element.text
return normal
parser = argparse.ArgumentParser(
prog="format", description="format postcards with latex"
)
2023-09-24 05:06:59 +00:00
parser.add_argument("template", help="template to use", nargs="?", default="2card")
2023-09-24 00:28:46 +00:00
parser.add_argument(
"-o", "--origin", help="origin country code", default="us", type=iso_code
)
parser.add_argument(
"-l",
"--language",
help="language to use for countries ",
default="en",
type=iso_code,
)
parser.add_argument(
"-c",
"--count",
default=1,
type=int,
help="Number of sets of labels to print, default 1 (labels only)",
)
parser.add_argument(
"-s",
"--skip",
default=0,
type=int,
help="Number of labels to skip (label sheets only)",
)
parser.add_argument(
"-a",
"--address-file",
default="addresses.csv",
type=str,
help="CSV file containing addresses",
)
parser.add_argument(
"-n",
"--no-content",
action="store_true",
help="Skip content, e.g. to make postcard back labels",
)
2023-10-01 02:13:40 +00:00
parser.add_argument(
"-d", "--dont-compile", action="store_true", help="Don't compile to output.pdf"
)
2023-09-24 00:28:46 +00:00
args = parser.parse_args()
2023-10-01 02:13:40 +00:00
cldr_root = ET.parse(
2023-09-24 00:28:46 +00:00
f"{os.getenv('CLDR_ROOT')}/share/unicode/cldr/common/main/{args.language}.xml"
)
csvfile = open(args.address_file)
rows = csv.DictReader(csvfile)
with open("secrets.json") as secrets_file:
secrets = json.load(secrets_file)
2023-09-24 00:28:46 +00:00
current_serial = imb.get_first_serial()
mid = secrets.get("mailer_id")
2023-09-24 00:28:46 +00:00
cards = []
for row in rows:
if row["Address"] == "":
continue
country = (
[]
if row["Country"].lower() == args.origin
2023-10-01 02:13:40 +00:00
else [get_country_name(cldr_root, row["Country"]).upper()]
2023-09-24 00:28:46 +00:00
)
address = row["Address"].split("\n") + country
if row.get("Avatar", "") != "":
avatar = get_avatar(row["Avatar"], secrets)
2023-09-24 00:28:46 +00:00
else:
avatar = None
cards += [
{
"address": "\n".join(address),
"avatar": avatar,
"row": row,
"imb": "",
}
]
cards = cards * args.count
serial = imb.get_first_serial()
if args.origin == "us":
for card in cards:
dpc = card["row"].get("DPC", "")
if dpc != "" and mid is not None:
2023-09-24 00:28:46 +00:00
card["imb"] = imb.generate(
0, 310, mid, serial, dpc.replace(" ", "").replace("-", "")
)
serial += 1
imb.write_current_serial(serial)
with open("options.json", "w") as options:
json.dump(
fp=options,
obj={
"args": args.__dict__,
"cards": cards,
},
)
2023-10-01 02:13:40 +00:00
if args.dont_compile:
exit()
os.execlp("typst", "typst", "compile", args.template, "output.pdf")