diff --git a/gen_images.py b/gen_images.py new file mode 100644 index 0000000..44d6009 --- /dev/null +++ b/gen_images.py @@ -0,0 +1,534 @@ +#!/usr/bin/env python3 +"""Pobiera zdjęcia obiektów noclegowych do katalogu pages/img/. + +Dla każdej oferty (`type: oferta*`) z polem `www` we frontmatterze skrypt +otwiera stronę obiektu i wyciąga z niej 3–5 zdjęć: + + 1. og:image / twitter:image — zdjęcie, które obiekt sam wybrał na wizytówkę, + 2. galeria ze strony — , + oraz background-image w stylach inline. + +Kandydaci są odsiewani dwoma filtrami, w tej kolejności: po nazwie pliku +(logotypy, ikony, mapy, piksele śledzące) i po samym pliku — waga > 30 KB +oraz proporcje w przedziale krajobrazowym (patrz MIN_RATIO/MAX_RATIO, +MIN_WIDTH). Co przejdzie, ląduje jako JPEG przeskalowany do MAX_WIDTH px +szerokości, jeśli w systemie jest Pillow; bez Pillow zapisywany jest oryginał +z własnym rozszerzeniem. + +Zapis: pages/img//01.jpg, 02.jpg, ... — `` to pole `id` +z frontmattera. Mapowanie oferta → zdjęcia jest wyłącznie konwencją +katalogową: pliki .md NIE są modyfikowane, tak samo jak w gen_pages.py +i gen_viz.py. Katalog czyta potem gen_pages.py (galeria na karcie, miniatury +w tabeli porównawczej, tło kafli regionów). + +Błąd jednej oferty (strona nie oddaje zdjęć, blokada scrapingu, timeout) nie +przerywa reszty — trafia na listę braków wypisywaną na końcu. Znany przypadek: +booking.com odrzuca automatyczne pobrania i taka oferta zostaje bez zdjęć. + +Uruchomienie: + python3 gen_images.py # pomija oferty, które mają już zdjęcia + python3 gen_images.py --force # pobiera wszystko od nowa + python3 gen_images.py --only kristall-schattbergxpress +""" + +from __future__ import annotations + +import argparse +import io +import re +import ssl +import sys +import urllib.error +import urllib.request +from dataclasses import dataclass, field +from html.parser import HTMLParser +from pathlib import Path +from urllib.parse import unquote, urljoin, urlsplit + +import yaml + +BUNDLE_ROOT = Path(__file__).resolve().parent +IMG_DIR = BUNDLE_ROOT / "pages" / "img" + +WANTED = 5 # ile zdjęć chcemy na obiekt +MIN_WANTED = 3 # poniżej tego oferta idzie na listę „niepełnych” +MAX_CANDIDATES = 40 # ile adresów maksymalnie próbujemy pobrać +MIN_BYTES = 30 * 1024 +MIN_WIDTH = 600 +MIN_RATIO = 0.85 # wysokość ≥ szerokości to zwykle baner/logo w pionie +MAX_RATIO = 2.60 # a bardzo szerokie paski to nagłówki, nie zdjęcia +MAX_TRANSPARENT = 0.03 # udział pikseli z alfą — powyżej to logo/plakietka +MAX_FLAT = 0.35 # udział jednego koloru — powyżej to grafika, nie zdjęcie +MAX_WIDTH = 1200 +TIMEOUT = 20 + +USER_AGENT = ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/126.0 Safari/537.36" +) + +IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".webp") + +_RETINA_RE = re.compile(r"@[234]x(?=\.[a-z]+$)") +MAX_HASH_DISTANCE = 10 # dystans Hamminga, poniżej którego to to samo zdjęcie + +# Nazwy plików, które prawie nigdy nie są zdjęciem obiektu. +NOISE_RE = re.compile( + r"(logo|icon|favicon|sprite|pixel|tracking|placeholder|avatar|banner|" + r"button|arrow|flag|map|karte|mappa|plan|piktogram|wetter|weather|" + r"footer|header|badge|award|siegel|social|whatsapp|facebook|instagram|" + r"partner|gutschein|zertifikat|prospekt|katalog)", + re.I, +) + +try: + from PIL import Image +except ModuleNotFoundError: # pragma: no cover + Image = None + +PILLOW = Image is not None + + +# --- oferty z bundle'a ------------------------------------------------- + + +@dataclass +class Offer: + concept_id: str + offer_id: str + name: str + url: str + operator: str = "" + + +def load_offers() -> list[Offer]: + offers: list[Offer] = [] + for path in sorted(BUNDLE_ROOT.rglob("*.md")): + if ".git" in path.parts or "pages" in path.parts: + continue + text = path.read_text(encoding="utf-8") + if not text.startswith("---\n"): + continue + end = text.find("\n---", 3) + if end == -1: + continue + frontmatter = yaml.safe_load(text[4:end]) or {} + if not str(frontmatter.get("type", "")).startswith("oferta"): + continue + offers.append( + Offer( + concept_id=path.relative_to(BUNDLE_ROOT).with_suffix("").as_posix(), + offer_id=str(frontmatter.get("id") or "").strip(), + name=str(frontmatter.get("nazwa") or frontmatter.get("title") or ""), + url=str(frontmatter.get("www") or "").strip(), + operator=str(frontmatter.get("operator") or ""), + ) + ) + return offers + + +# --- pobieranie -------------------------------------------------------- + +_LAX_SSL = ssl.create_default_context() +_LAX_SSL.check_hostname = False +_LAX_SSL.verify_mode = ssl.CERT_NONE + + +def fetch(url: str, referer: str = "") -> tuple[bytes, str]: + """Zwraca (bajty, content-type). Rzuca OSError/HTTPError przy porażce.""" + headers = { + "User-Agent": USER_AGENT, + "Accept": "text/html,application/xhtml+xml,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "pl,en;q=0.8,de;q=0.6", + } + if referer: + headers["Referer"] = referer + request = urllib.request.Request(url, headers=headers) + try: + with urllib.request.urlopen(request, timeout=TIMEOUT) as response: + return response.read(), response.headers.get("Content-Type", "") + except urllib.error.URLError as exc: + # Część małych obiektów ma łańcuch certyfikatów niepełny — dla zdjęć + # z publicznej strony to akceptowalne ryzyko, więc jedna próba obok. + if isinstance(getattr(exc, "reason", None), ssl.SSLError): + with urllib.request.urlopen( + request, timeout=TIMEOUT, context=_LAX_SSL + ) as response: + return response.read(), response.headers.get("Content-Type", "") + raise + + +# --- wyciąganie adresów zdjęć ze strony -------------------------------- + + +class ImageCollector(HTMLParser): + """Zbiera kandydatów na zdjęcia: og:image, , , tła.""" + + def __init__(self) -> None: + super().__init__(convert_charrefs=True) + self.meta: list[str] = [] + self.gallery: list[str] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + data = {k.lower(): (v or "") for k, v in attrs} + if tag == "meta": + key = (data.get("property") or data.get("name") or "").lower() + if key in {"og:image", "og:image:url", "og:image:secure_url", "twitter:image"}: + self._add(self.meta, data.get("content", "")) + elif tag == "img": + for attribute in ("data-src", "data-lazy-src", "data-original", "src"): + if data.get(attribute): + self._add(self.gallery, data[attribute]) + break + for attribute in ("srcset", "data-srcset"): + for part in data.get(attribute, "").split(","): + url = part.strip().split(" ")[0] + self._add(self.gallery, url) + elif tag == "a": + href = data.get("href", "") + if href.split("?")[0].lower().endswith(IMAGE_EXTS): + self._add(self.gallery, href) + if "style" in data: + for match in re.finditer(r"url\(['\"]?([^)'\"]+)", data["style"]): + self._add(self.gallery, match.group(1)) + + def _add(self, bucket: list[str], url: str) -> None: + url = (url or "").strip() + if url and not url.startswith("data:") and url not in bucket: + bucket.append(url) + + +def candidates(page_url: str, markup: str, tokens: list[str] = ()) -> list[str]: + collector = ImageCollector() + try: + collector.feed(markup) + except Exception: # parser HTML-a nie może wywrócić całego przebiegu + pass + + ordered: list[str] = [] + seen: set[str] = set() + for url in collector.meta + collector.gallery: + absolute = urljoin(page_url, url) + if not absolute.lower().startswith(("http://", "https://")): + continue + path = urlsplit(absolute).path + if path.lower().endswith(".svg") or NOISE_RE.search(path): + continue + # To samo zdjęcie wraca w srcset w kilku gęstościach (@2x, @3x) + # i z parametrami skalowania w query — liczy się raz. + key = _RETINA_RE.sub("", unquote(path).lower()) + if key in seen: + continue + seen.add(key) + ordered.append(absolute) + + # Na stronach portalowych (marka z wieloma obiektami, katalog noclegów) + # w tym samym HTML-u siedzą zdjęcia cudzych obiektów. Jeśli którykolwiek + # adres niesie w ścieżce nazwę własną obiektu, bierzemy tylko takie — + # a gdy żaden jej nie niesie, zostaje pełna lista (strona własna obiektu). + if tokens: + relevant = [ + url + for url in ordered + if any(token in unquote(urlsplit(url).path).lower() for token in tokens) + ] + if relevant: + ordered = relevant + return ordered[:MAX_CANDIDATES] + + +# Człony nazw, które nie identyfikują obiektu — są w co drugiej ofercie. +GENERIC_TOKENS = { + "apartment", "apartments", "appartement", "appartementhaus", "apart", + "hotel", "house", "residence", "chalet", "lodge", "ferienwohnung", + "wohnung", "central", "sport", "sportchalet", +} + + +def object_tokens(offer: Offer) -> list[str]: + """Nazwy własne obiektu, po których poznamy jego zdjęcia na portalu. + + Odpadają człony rodzajowe („apartments”), nazwa operatora („AlpenParks”) + i wszystko, co siedzi już w domenie — jeśli cała witryna należy do + obiektu, filtrowanie po nazwie nie ma czego zawężać. + """ + host = urlsplit(offer.url).netloc.lower().replace("-", "").replace(".", "") + brand = re.sub(r"[^a-z0-9]", "", (offer.operator or "").lower()) + tokens = [] + for part in re.split(r"[^a-z0-9]+", offer.offer_id.lower()): + if len(part) < 5 or part in GENERIC_TOKENS: + continue + if part in host or (brand and part in brand): + continue + tokens.append(part) + return tokens + + +def object_page(page_url: str, markup: str, tokens: list[str]) -> str: + """Podstrona obiektu wylinkowana ze strony głównej marki/portalu.""" + if not tokens or urlsplit(page_url).path.strip("/"): + return "" # weszliśmy już w konkretną podstronę, nie w korzeń + for match in re.finditer(r'href=["\']([^"\'#]+)["\']', markup, re.I): + target = urljoin(page_url, match.group(1)) + if urlsplit(target).netloc != urlsplit(page_url).netloc: + continue + path = unquote(urlsplit(target).path).lower() + if path.strip("/") and any(token in path for token in tokens): + return target + return "" + + +def decode(raw: bytes, content_type: str) -> str: + match = re.search(r"charset=([\w-]+)", content_type, re.I) + if not match: + match = re.search(rb'charset=["\']?([\w-]+)', raw[:4096], re.I) + encoding = match.group(1).decode("ascii", "ignore") if match else "utf-8" + else: + encoding = match.group(1) + try: + return raw.decode(encoding, "replace") + except LookupError: + return raw.decode("utf-8", "replace") + + +# --- zapis ------------------------------------------------------------- + + +def acceptable(raw: bytes, seen: list[int] = ()) -> tuple[bool, str, int]: + """(czy bierzemy, powód odrzucenia, odcisk percepcyjny).""" + if raw[:1024].lstrip().lower().startswith((b" MAX_TRANSPARENT: + return False, f"grafika z przezroczystością ({transparent:.0%})", 0 + if flat > MAX_FLAT: + return False, f"płaskie tło ({flat:.0%} jednego koloru) — grafika", 0 + # To samo zdjęcie bywa na stronie pod kilkoma adresami i w kilku + # rozdzielczościach — po odcisku poznajemy je niezależnie od pliku. + for other in seen: + if bin(fingerprint ^ other).count("1") <= MAX_HASH_DISTANCE: + return False, "duplikat wcześniejszego zdjęcia", fingerprint + return True, "", fingerprint + + +def _ahash(image: "Image.Image") -> int: + """Odcisk percepcyjny (average hash) — odporny na zmianę rozdzielczości.""" + pixels = image.convert("L").resize((8, 8)).tobytes() + average = sum(pixels) / len(pixels) + return sum(1 << n for n, value in enumerate(pixels) if value > average) + + +def _transparency(image: "Image.Image") -> float: + if image.mode not in ("RGBA", "LA", "P") or ( + image.mode == "P" and "transparency" not in image.info + ): + return 0.0 + alpha = image.convert("RGBA").getchannel("A").resize((48, 48)) + histogram = alpha.getcolors(256) or [] + total = sum(count for count, _ in histogram) or 1 + return sum(count for count, value in histogram if value < 250) / total + + +def _flatness(image: "Image.Image") -> float: + small = image.convert("RGB").resize((48, 48)) + histogram = small.getcolors(48 * 48) + if not histogram: # więcej kolorów niż pikseli próbki — na pewno zdjęcie + return 0.0 + total = sum(count for count, _ in histogram) or 1 + return max(count for count, _ in histogram) / total + + +def save(raw: bytes, directory: Path, index: int, source_url: str) -> Path: + directory.mkdir(parents=True, exist_ok=True) + if PILLOW: + with Image.open(io.BytesIO(raw)) as image: + image = image.convert("RGB") + if image.width > MAX_WIDTH: + height = round(image.height * MAX_WIDTH / image.width) + image = image.resize((MAX_WIDTH, height), Image.LANCZOS) + out = directory / f"{index:02d}.jpg" + image.save(out, "JPEG", quality=82, optimize=True, progressive=True) + return out + suffix = Path(urlsplit(source_url).path).suffix.lower() + out = directory / f"{index:02d}{suffix if suffix in IMAGE_EXTS else '.jpg'}" + out.write_bytes(raw) + return out + + +# --- przebieg na ofertę ------------------------------------------------ + + +@dataclass +class Result: + offer: Offer + saved: list[Path] = field(default_factory=list) + reason: str = "" + source: str = "" # podstrona obiektu, jeśli weszliśmy głębiej niż `www` + + +def collect(offer: Offer, force: bool) -> Result: + result = Result(offer=offer) + directory = IMG_DIR / offer.offer_id + + if not offer.offer_id: + result.reason = "brak pola `id` we frontmatterze" + return result + if not offer.url: + result.reason = "brak pola `www` (oferta bez konkretnego obiektu)" + return result + + existing = sorted(directory.glob("*")) if directory.is_dir() else [] + if existing and not force: + result.saved = existing + result.reason = "pominięte — zdjęcia już są (--force nadpisze)" + return result + + try: + raw, content_type = fetch(offer.url) + except Exception as exc: + result.reason = f"strona nie odpowiada: {_describe(exc)}" + return result + + markup = decode(raw, content_type) + source_url = offer.url + tokens = object_tokens(offer) + deeper = object_page(offer.url, markup, tokens) + if deeper: + try: + raw, content_type = fetch(deeper) + markup = decode(raw, content_type) + source_url = deeper + result.source = deeper + except Exception: + pass # zostajemy przy stronie głównej + + urls = candidates(source_url, markup, tokens) + if not urls: + result.reason = "strona nie zawiera kandydatów na zdjęcia (HTML bez galerii)" + return result + + if force and directory.is_dir(): + for old in directory.glob("*"): + old.unlink() + + rejected: list[str] = [] + hashes: list[int] = [] + for url in urls: + if len(result.saved) >= WANTED: + break + try: + image_raw, image_type = fetch(url, referer=offer.url) + except Exception as exc: + rejected.append(f"{_short(url)}: {_describe(exc)}") + continue + if "image" not in image_type and not urlsplit(url).path.lower().endswith( + IMAGE_EXTS + ): + rejected.append(f"{_short(url)}: nie obrazek ({image_type})") + continue + ok, why, fingerprint = acceptable(image_raw, hashes) + if not ok: + rejected.append(f"{_short(url)}: {why}") + continue + hashes.append(fingerprint) + try: + result.saved.append( + save(image_raw, directory, len(result.saved) + 1, url) + ) + except Exception as exc: + rejected.append(f"{_short(url)}: zapis — {_describe(exc)}") + + if not result.saved: + head = "; ".join(rejected[:3]) or "brak przyczyny" + result.reason = f"żaden kandydat nie przeszedł filtrów ({head})" + elif len(result.saved) < MIN_WANTED: + result.reason = f"tylko {len(result.saved)} zdj. (cel: {MIN_WANTED}–{WANTED})" + return result + + +def _describe(exc: Exception) -> str: + if isinstance(exc, urllib.error.HTTPError): + return f"HTTP {exc.code}" + reason = getattr(exc, "reason", None) + return f"{type(exc).__name__}: {reason or exc}" + + +def _short(url: str) -> str: + name = urlsplit(url).path.rsplit("/", 1)[-1] or urlsplit(url).netloc + return name[:42] + + +# --- main -------------------------------------------------------------- + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--force", action="store_true", help="pobierz od nowa") + parser.add_argument("--only", metavar="ID", help="tylko ta oferta (pole `id`)") + args = parser.parse_args() + + offers = load_offers() + if args.only: + offers = [o for o in offers if o.offer_id == args.only] + if not offers: + print(f"Nie ma oferty o id={args.only}", file=sys.stderr) + return 1 + + print(f"Pillow: {'jest — skalowanie do ' + str(MAX_WIDTH) + 'px' if PILLOW else 'BRAK — zapis oryginałów'}") + print(f"Ofert do przetworzenia: {len(offers)}\n") + + results = [] + for offer in offers: + print(f"→ {offer.offer_id or offer.concept_id} ({offer.url or 'brak www'})") + result = collect(offer, args.force) + results.append(result) + if result.saved: + names = ", ".join(p.name for p in result.saved) + print(f" {len(result.saved)} zdj.: {names}" + + (f" [{result.reason}]" if result.reason else "")) + else: + print(f" ✗ {result.reason}") + + ok = [r for r in results if len(r.saved) >= MIN_WANTED] + partial = [r for r in results if 0 < len(r.saved) < MIN_WANTED] + missing = [r for r in results if not r.saved] + + print(f"\nZe zdjęciami ({len(ok)}):") + for result in ok: + print(f" + {result.offer.offer_id} — {len(result.saved)}") + if partial: + print(f"\nNiepełne ({len(partial)}):") + for result in partial: + print(f" ~ {result.offer.offer_id} — {result.reason}") + if missing: + print(f"\nBez zdjęć ({len(missing)}):") + for result in missing: + print(f" ! {result.offer.offer_id or result.offer.concept_id} — {result.reason}") + + total = sum(p.stat().st_size for p in IMG_DIR.rglob("*") if p.is_file()) + print(f"\nRazem w {IMG_DIR.relative_to(BUNDLE_ROOT)}: {total / 1024 / 1024:.1f} MiB") + print("Teraz: python3 gen_pages.py (wpina zdjęcia w karty i tabelę)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pages/img/alpenparks-rehrenberg/01.jpg b/pages/img/alpenparks-rehrenberg/01.jpg new file mode 100644 index 0000000..8f1d45d Binary files /dev/null and b/pages/img/alpenparks-rehrenberg/01.jpg differ diff --git a/pages/img/alpenparks-rehrenberg/02.jpg b/pages/img/alpenparks-rehrenberg/02.jpg new file mode 100644 index 0000000..350c96b Binary files /dev/null and b/pages/img/alpenparks-rehrenberg/02.jpg differ diff --git a/pages/img/alpenparks-rehrenberg/03.jpg b/pages/img/alpenparks-rehrenberg/03.jpg new file mode 100644 index 0000000..52607ff Binary files /dev/null and b/pages/img/alpenparks-rehrenberg/03.jpg differ diff --git a/pages/img/alpenparks-rehrenberg/04.jpg b/pages/img/alpenparks-rehrenberg/04.jpg new file mode 100644 index 0000000..4eb7e47 Binary files /dev/null and b/pages/img/alpenparks-rehrenberg/04.jpg differ diff --git a/pages/img/alpenparks-rehrenberg/05.jpg b/pages/img/alpenparks-rehrenberg/05.jpg new file mode 100644 index 0000000..cf6e4ab Binary files /dev/null and b/pages/img/alpenparks-rehrenberg/05.jpg differ diff --git a/pages/img/alpenparks-sonnleiten/01.jpg b/pages/img/alpenparks-sonnleiten/01.jpg new file mode 100644 index 0000000..d41fbf6 Binary files /dev/null and b/pages/img/alpenparks-sonnleiten/01.jpg differ diff --git a/pages/img/alpenparks-sonnleiten/02.jpg b/pages/img/alpenparks-sonnleiten/02.jpg new file mode 100644 index 0000000..fb864dc Binary files /dev/null and b/pages/img/alpenparks-sonnleiten/02.jpg differ diff --git a/pages/img/alpenparks-sonnleiten/03.jpg b/pages/img/alpenparks-sonnleiten/03.jpg new file mode 100644 index 0000000..468510f Binary files /dev/null and b/pages/img/alpenparks-sonnleiten/03.jpg differ diff --git a/pages/img/alpenparks-sonnleiten/04.jpg b/pages/img/alpenparks-sonnleiten/04.jpg new file mode 100644 index 0000000..6fb265d Binary files /dev/null and b/pages/img/alpenparks-sonnleiten/04.jpg differ diff --git a/pages/img/alpenparks-sonnleiten/05.jpg b/pages/img/alpenparks-sonnleiten/05.jpg new file mode 100644 index 0000000..8469138 Binary files /dev/null and b/pages/img/alpenparks-sonnleiten/05.jpg differ diff --git a/pages/img/apartment-brandau-kappl/01.jpg b/pages/img/apartment-brandau-kappl/01.jpg new file mode 100644 index 0000000..735648c Binary files /dev/null and b/pages/img/apartment-brandau-kappl/01.jpg differ diff --git a/pages/img/apartment-brandau-kappl/02.jpg b/pages/img/apartment-brandau-kappl/02.jpg new file mode 100644 index 0000000..4427336 Binary files /dev/null and b/pages/img/apartment-brandau-kappl/02.jpg differ diff --git a/pages/img/apartment-brandau-kappl/03.jpg b/pages/img/apartment-brandau-kappl/03.jpg new file mode 100644 index 0000000..83de3af Binary files /dev/null and b/pages/img/apartment-brandau-kappl/03.jpg differ diff --git a/pages/img/apartment-brandau-kappl/04.jpg b/pages/img/apartment-brandau-kappl/04.jpg new file mode 100644 index 0000000..518a8cc Binary files /dev/null and b/pages/img/apartment-brandau-kappl/04.jpg differ diff --git a/pages/img/apartment-brandau-kappl/05.jpg b/pages/img/apartment-brandau-kappl/05.jpg new file mode 100644 index 0000000..23655d6 Binary files /dev/null and b/pages/img/apartment-brandau-kappl/05.jpg differ diff --git a/pages/img/apartments-kappl/01.jpg b/pages/img/apartments-kappl/01.jpg new file mode 100644 index 0000000..309ba6d Binary files /dev/null and b/pages/img/apartments-kappl/01.jpg differ diff --git a/pages/img/apartments-kappl/02.jpg b/pages/img/apartments-kappl/02.jpg new file mode 100644 index 0000000..51fee60 Binary files /dev/null and b/pages/img/apartments-kappl/02.jpg differ diff --git a/pages/img/apartments-kappl/03.jpg b/pages/img/apartments-kappl/03.jpg new file mode 100644 index 0000000..27123ff Binary files /dev/null and b/pages/img/apartments-kappl/03.jpg differ diff --git a/pages/img/apartments-kappl/04.jpg b/pages/img/apartments-kappl/04.jpg new file mode 100644 index 0000000..846f448 Binary files /dev/null and b/pages/img/apartments-kappl/04.jpg differ diff --git a/pages/img/apartments-kappl/05.jpg b/pages/img/apartments-kappl/05.jpg new file mode 100644 index 0000000..8d33d6c Binary files /dev/null and b/pages/img/apartments-kappl/05.jpg differ diff --git a/pages/img/glemm-lodge/01.jpg b/pages/img/glemm-lodge/01.jpg new file mode 100644 index 0000000..9d1832c Binary files /dev/null and b/pages/img/glemm-lodge/01.jpg differ diff --git a/pages/img/glemm-lodge/02.jpg b/pages/img/glemm-lodge/02.jpg new file mode 100644 index 0000000..94bffa4 Binary files /dev/null and b/pages/img/glemm-lodge/02.jpg differ diff --git a/pages/img/glemm-lodge/03.jpg b/pages/img/glemm-lodge/03.jpg new file mode 100644 index 0000000..2254d41 Binary files /dev/null and b/pages/img/glemm-lodge/03.jpg differ diff --git a/pages/img/glemm-lodge/04.jpg b/pages/img/glemm-lodge/04.jpg new file mode 100644 index 0000000..b863695 Binary files /dev/null and b/pages/img/glemm-lodge/04.jpg differ diff --git a/pages/img/glemm-lodge/05.jpg b/pages/img/glemm-lodge/05.jpg new file mode 100644 index 0000000..6989935 Binary files /dev/null and b/pages/img/glemm-lodge/05.jpg differ diff --git a/pages/img/haeuslerhof-valdaora/01.jpg b/pages/img/haeuslerhof-valdaora/01.jpg new file mode 100644 index 0000000..f75d9df Binary files /dev/null and b/pages/img/haeuslerhof-valdaora/01.jpg differ diff --git a/pages/img/haeuslerhof-valdaora/02.jpg b/pages/img/haeuslerhof-valdaora/02.jpg new file mode 100644 index 0000000..49d37d6 Binary files /dev/null and b/pages/img/haeuslerhof-valdaora/02.jpg differ diff --git a/pages/img/haeuslerhof-valdaora/03.jpg b/pages/img/haeuslerhof-valdaora/03.jpg new file mode 100644 index 0000000..d5fba83 Binary files /dev/null and b/pages/img/haeuslerhof-valdaora/03.jpg differ diff --git a/pages/img/haeuslerhof-valdaora/04.jpg b/pages/img/haeuslerhof-valdaora/04.jpg new file mode 100644 index 0000000..04b7727 Binary files /dev/null and b/pages/img/haeuslerhof-valdaora/04.jpg differ diff --git a/pages/img/haus-central-ladis/01.jpg b/pages/img/haus-central-ladis/01.jpg new file mode 100644 index 0000000..4e33b83 Binary files /dev/null and b/pages/img/haus-central-ladis/01.jpg differ diff --git a/pages/img/haus-central-ladis/02.jpg b/pages/img/haus-central-ladis/02.jpg new file mode 100644 index 0000000..aae72e7 Binary files /dev/null and b/pages/img/haus-central-ladis/02.jpg differ diff --git a/pages/img/haus-central-ladis/03.jpg b/pages/img/haus-central-ladis/03.jpg new file mode 100644 index 0000000..4f8fd8e Binary files /dev/null and b/pages/img/haus-central-ladis/03.jpg differ diff --git a/pages/img/haus-central-ladis/04.jpg b/pages/img/haus-central-ladis/04.jpg new file mode 100644 index 0000000..f8f80fa Binary files /dev/null and b/pages/img/haus-central-ladis/04.jpg differ diff --git a/pages/img/haus-central-ladis/05.jpg b/pages/img/haus-central-ladis/05.jpg new file mode 100644 index 0000000..9f6ee59 Binary files /dev/null and b/pages/img/haus-central-ladis/05.jpg differ diff --git a/pages/img/sportchalet-viehhofen/01.jpg b/pages/img/sportchalet-viehhofen/01.jpg new file mode 100644 index 0000000..64411df Binary files /dev/null and b/pages/img/sportchalet-viehhofen/01.jpg differ diff --git a/pages/img/sportchalet-viehhofen/02.jpg b/pages/img/sportchalet-viehhofen/02.jpg new file mode 100644 index 0000000..b45df46 Binary files /dev/null and b/pages/img/sportchalet-viehhofen/02.jpg differ diff --git a/pages/img/sportchalet-viehhofen/03.jpg b/pages/img/sportchalet-viehhofen/03.jpg new file mode 100644 index 0000000..dbf93b9 Binary files /dev/null and b/pages/img/sportchalet-viehhofen/03.jpg differ diff --git a/pages/img/sportchalet-viehhofen/04.jpg b/pages/img/sportchalet-viehhofen/04.jpg new file mode 100644 index 0000000..47597aa Binary files /dev/null and b/pages/img/sportchalet-viehhofen/04.jpg differ diff --git a/pages/img/sportchalet-viehhofen/05.jpg b/pages/img/sportchalet-viehhofen/05.jpg new file mode 100644 index 0000000..a841b4c Binary files /dev/null and b/pages/img/sportchalet-viehhofen/05.jpg differ