From f7fba60ebfea0797e2683facf4ab2ce61ed29977 Mon Sep 17 00:00:00 2001 From: patrick3399 <99384328+patrick3399@users.noreply.github.com> Date: Sun, 8 Mar 2026 20:41:26 +0800 Subject: [PATCH] Normalize positive Y offsets for EH sprite previews --- backend/services/cache.py | 4 +-- backend/services/eh_client.py | 42 ++++++++++++----------- pwa/src/app/browse/[gid]/[token]/page.tsx | 20 +++++++++-- pwa/src/components/Reader/index.tsx | 17 +++++---- pwa/src/lib/types.ts | 2 +- 5 files changed, 53 insertions(+), 32 deletions(-) diff --git a/backend/services/cache.py b/backend/services/cache.py index 7f3d96a9..14fee587 100644 --- a/backend/services/cache.py +++ b/backend/services/cache.py @@ -64,11 +64,11 @@ async def set_imagelist_cache(gid: int, data: dict) -> None: async def get_preview_cache(gid: int) -> dict | None: """Returns {str(page_num): preview_url_or_sprite_info} or None.""" - return await get_json(f"eh:previews:{gid}") + return await get_json(f"eh:previews:v2:{gid}") async def set_preview_cache(gid: int, data: dict) -> None: - await set_json(f"eh:previews:{gid}", data, _TTL_IMAGELIST) + await set_json(f"eh:previews:v2:{gid}", data, _TTL_IMAGELIST) async def get_proxied_image(gid: int, page: int) -> bytes | None: diff --git a/backend/services/eh_client.py b/backend/services/eh_client.py index 7ab9211f..d9c0cd07 100644 --- a/backend/services/eh_client.py +++ b/backend/services/eh_client.py @@ -51,22 +51,22 @@ r']*alt="(\d+)"[^>]*src="([^"]+)"', re.DOTALL, ) -# Normal preview:
-# We extract the sprite URL, offset, width, height AND the page number from the /s/ link inside +# Normal preview:
+# We extract the sprite URL, X/Y offsets, width, height AND the page number from the /s/ link inside _NORMAL_PREVIEW_RE = re.compile( r']*class="gdtm"[^>]*style="[^"]*' - r'url\(([^)]+)\)\s*(-?\d+)px[^"]*' + r'url\(([^)]+)\)\s*(-?\d+)px\s+(-?\d+)px[^"]*' r'width:\s*(\d+)px;\s*height:\s*(\d+)px[^"]*"[^>]*>' r".*?/s/[0-9a-f]+/\d+-(\d+)", re.DOTALL, ) -# New format (2024+):
-# Order in style: width → height → background (with url + offset) +# New format (2024+):
+# Order in style: width → height → background (with url + X/Y offsets) _NEW_PREVIEW_RE = re.compile( r']+href="[^"]+/s/[0-9a-f]{10}/\d+-(\d+)"[^>]*>' r']+style="[^"]*' r'width:\s*(\d+)px[^"]*height:\s*(\d+)px[^"]*' - r'url\(([^)]+)\)\s*(-?\d+)px', + r'url\(([^)]+)\)\s*(-?\d+)px\s+(-?\d+)px', re.DOTALL, ) @@ -296,16 +296,17 @@ def _parse_detail_html(self, html: str) -> tuple[dict[int, str], dict[int, str]] # Extract preview thumbnails — try new format first (2024+), then legacy formats new_matches = list(_NEW_PREVIEW_RE.finditer(html)) if new_matches: - # New format: background url() with optional sprite offset - # Groups: (page_num, width, height, thumb_url, offset_x) + # New format: background url() with sprite offsets + # Groups: (page_num, width, height, thumb_url, offset_x, offset_y) for match in new_matches: page_num = int(match.group(1)) width = int(match.group(2)) height = int(match.group(3)) thumb_url = match.group(4) offset_x = int(match.group(5)) - # Always store as sprite format — even offset 0 is part of the sprite sheet - preview_map[page_num] = f"{thumb_url}|{offset_x}|{width}|{height}" + offset_y = int(match.group(6)) + # Always store as sprite format — even zero offsets are part of the sprite sheet + preview_map[page_num] = f"{thumb_url}|{offset_x}|{offset_y}|{width}|{height}" # Normalize cell heights per sprite URL — the sprite image has a single # height, but CSS may declare different heights for individual cells @@ -313,18 +314,18 @@ def _parse_detail_html(self, html: str) -> tuple[dict[int, str], dict[int, str]] sprite_heights: dict[str, int] = defaultdict(int) for page_num, val in preview_map.items(): parts = val.split('|') - if len(parts) == 4: + if len(parts) == 5: sprite_url = parts[0] - h = int(parts[3]) + h = int(parts[4]) if h > sprite_heights[sprite_url]: sprite_heights[sprite_url] = h for page_num in list(preview_map.keys()): parts = preview_map[page_num].split('|') - if len(parts) == 4: + if len(parts) == 5: sprite_url = parts[0] max_h = sprite_heights[sprite_url] - if int(parts[3]) != max_h: - preview_map[page_num] = f"{parts[0]}|{parts[1]}|{parts[2]}|{max_h}" + if int(parts[4]) != max_h: + preview_map[page_num] = f"{parts[0]}|{parts[1]}|{parts[2]}|{parts[3]}|{max_h}" else: # Legacy large previews:
N large_matches = list(_LARGE_PREVIEW_RE.finditer(html)) @@ -335,14 +336,15 @@ def _parse_detail_html(self, html: str) -> tuple[dict[int, str], dict[int, str]] preview_map[page_num] = thumb_url else: # Legacy normal previews (CSS sprite sheets with gdtm class) - # Store as "url|offsetX|width|height" for frontend to render + # Store as "url|offsetX|offsetY|width|height" for frontend to render for match in _NORMAL_PREVIEW_RE.finditer(html): sprite_url = match.group(1) offset_x = int(match.group(2)) - width = int(match.group(3)) - height = int(match.group(4)) - page_num = int(match.group(5)) - preview_map[page_num] = f"{sprite_url}|{offset_x}|{width}|{height}" + offset_y = int(match.group(3)) + width = int(match.group(4)) + height = int(match.group(5)) + page_num = int(match.group(6)) + preview_map[page_num] = f"{sprite_url}|{offset_x}|{offset_y}|{width}|{height}" return token_map, preview_map diff --git a/pwa/src/app/browse/[gid]/[token]/page.tsx b/pwa/src/app/browse/[gid]/[token]/page.tsx index 5a3c54fd..15536cbd 100644 --- a/pwa/src/app/browse/[gid]/[token]/page.tsx +++ b/pwa/src/app/browse/[gid]/[token]/page.tsx @@ -17,7 +17,7 @@ function PreviewGrid({ thumbs, onRead, }: { - thumbs: { page: number; url: string; isSprite: boolean; offsetX?: number; width?: number; height?: number }[] + thumbs: { page: number; url: string; isSprite: boolean; offsetX?: number; offsetY?: number; width?: number; height?: number }[] onRead: (page: number) => void }) { const gridRef = useRef(null) @@ -44,6 +44,14 @@ function PreviewGrid({ const th = thumb.height ?? 300 const scale = cellSize.w ? cellSize.w / tw : 1 const scaledH = th * scale + const normalizedOffsetX = (() => { + const raw = thumb.offsetX ?? 0 + return raw > 0 ? -raw : raw + })() + const normalizedOffsetY = (() => { + const raw = thumb.offsetY ?? 0 + return raw > 0 ? -raw : raw + })() return (