]+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:

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 (