Add HTTP API server, Android app, and CI build/publish pipeline
build / build (push) Failing after 14s
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)
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user