516f5d3b7d
build / build (push) Failing after 14s
- docker-compose.yml + tooling-container/src/server.py: serve
preprocess/OCR/extract over an async job API (POST /jobs, GET
/jobs/{id}) instead of manual host-script runs per receipt
- android/: Kotlin/Compose app - capture a receipt photo, poll job
progress, review/edit extracted fields, save locally (Room), export
to CSV/ODS
- .gitea/workflows/build.yml: build the debug APK and publish it to
dl.ladkau.de on push
- build-image.sh: also pull ollama/ollama so all pipeline images are
ready after one script
- add project logo (also used for the app's launcher icon/splash)
116 lines
4.7 KiB
Python
116 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Generates the Android launcher icon and splash screen art from
|
|
../../logo.png (the repo's brand mark + "fuelish" wordmark).
|
|
|
|
Only the graphic mark (not the wordmark) works as a launcher/splash icon,
|
|
so this first isolates it: crop the upper portion of the source image
|
|
(where the mark lives) and auto-crop that to its actual non-transparent
|
|
bounding box, rather than relying on any hardcoded pixel offsets that
|
|
would break if logo.png is ever redrawn at a different resolution.
|
|
|
|
Produces:
|
|
- mipmap-<density>/ic_launcher_foreground.png - the mark centered, scaled
|
|
to the adaptive-icon safe zone (66% of a 108dp canvas), transparent
|
|
background - paired at runtime with @color/ic_launcher_background (see
|
|
mipmap-anydpi-v26/ic_launcher.xml, committed - a static reference, not
|
|
generated) for API 26+.
|
|
- mipmap-<density>/ic_launcher.png / ic_launcher_round.png - the same
|
|
mark flattened onto ic_launcher_background, for API <26 devices that
|
|
can't use the adaptive-icon XML at all.
|
|
- drawable-nodpi/splash_icon.png - the mark on a transparent background,
|
|
referenced by Theme.Fuelish.Splash's windowSplashScreenAnimatedIcon
|
|
(see values/themes.xml).
|
|
|
|
Generated output, not committed (see the repo's root .gitignore's
|
|
/android/app/src/main/res/{mipmap-*,drawable-nodpi} entries) - regenerate
|
|
by running this script directly or via Gradle's generateAppIcons task
|
|
(wired into app's preBuild, see ../app/build.gradle.kts). Requires
|
|
Pillow.
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
from PIL import Image
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
LOGO_PATH = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "logo.png"))
|
|
RES_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "app", "src", "main", "res"))
|
|
|
|
BACKGROUND_RGBA = (255, 255, 255, 255) # matches @color/ic_launcher_background
|
|
|
|
# (density qualifier, adaptive-icon canvas px, legacy launcher px)
|
|
DENSITIES = [
|
|
("mdpi", 108, 48),
|
|
("hdpi", 162, 72),
|
|
("xhdpi", 216, 96),
|
|
("xxhdpi", 324, 144),
|
|
("xxxhdpi", 432, 192),
|
|
]
|
|
|
|
# Adaptive icons render only the inner ~66% "safe zone" reliably across
|
|
# launchers that apply their own mask shape.
|
|
SAFE_ZONE_RATIO = 0.66
|
|
|
|
# Fraction of logo.png's height (from the top) that contains the graphic
|
|
# mark, before the "fuelish" wordmark begins.
|
|
MARK_REGION_HEIGHT_RATIO = 0.60
|
|
|
|
SPLASH_CANVAS_PX = 480
|
|
SPLASH_ICON_RATIO = 0.75
|
|
|
|
|
|
def extract_mark(logo: Image.Image) -> Image.Image:
|
|
width, height = logo.size
|
|
region = logo.crop((0, 0, width, round(height * MARK_REGION_HEIGHT_RATIO)))
|
|
bbox = region.getchannel("A").getbbox()
|
|
if bbox is None:
|
|
raise ValueError(f"{LOGO_PATH}: no non-transparent pixels found in the top "
|
|
f"{MARK_REGION_HEIGHT_RATIO:.0%} of the image")
|
|
mark = region.crop(bbox)
|
|
|
|
# Pad to square so downstream scaling doesn't distort the aspect ratio.
|
|
side = max(mark.width, mark.height)
|
|
square = Image.new("RGBA", (side, side), (0, 0, 0, 0))
|
|
square.paste(mark, ((side - mark.width) // 2, (side - mark.height) // 2), mark)
|
|
return square
|
|
|
|
|
|
def resized(image: Image.Image, target_px: int) -> Image.Image:
|
|
ratio = target_px / max(image.width, image.height)
|
|
size = (round(image.width * ratio), round(image.height * ratio))
|
|
return image.resize(size, Image.LANCZOS)
|
|
|
|
|
|
def centered_on(canvas_px: int, content: Image.Image, background: tuple) -> Image.Image:
|
|
canvas = Image.new("RGBA", (canvas_px, canvas_px), background)
|
|
offset = ((canvas_px - content.width) // 2, (canvas_px - content.height) // 2)
|
|
canvas.paste(content, offset, content)
|
|
return canvas
|
|
|
|
|
|
def main():
|
|
logo = Image.open(LOGO_PATH).convert("RGBA")
|
|
mark = extract_mark(logo)
|
|
|
|
for density, canvas_px, legacy_px in DENSITIES:
|
|
mipmap_dir = os.path.join(RES_DIR, f"mipmap-{density}")
|
|
os.makedirs(mipmap_dir, exist_ok=True)
|
|
|
|
foreground = centered_on(canvas_px, resized(mark, round(canvas_px * SAFE_ZONE_RATIO)), (0, 0, 0, 0))
|
|
foreground.save(os.path.join(mipmap_dir, "ic_launcher_foreground.png"))
|
|
|
|
legacy = centered_on(legacy_px, resized(mark, round(legacy_px * SAFE_ZONE_RATIO)), BACKGROUND_RGBA)
|
|
legacy.convert("RGB").save(os.path.join(mipmap_dir, "ic_launcher.png"))
|
|
legacy.convert("RGB").save(os.path.join(mipmap_dir, "ic_launcher_round.png"))
|
|
|
|
drawable_dir = os.path.join(RES_DIR, "drawable-nodpi")
|
|
os.makedirs(drawable_dir, exist_ok=True)
|
|
splash = centered_on(SPLASH_CANVAS_PX, resized(mark, round(SPLASH_CANVAS_PX * SPLASH_ICON_RATIO)), (0, 0, 0, 0))
|
|
splash.save(os.path.join(drawable_dir, "splash_icon.png"))
|
|
|
|
print(f"wrote launcher icons for {len(DENSITIES)} densities + splash icon to {RES_DIR}", file=sys.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|